amine_dubs
commited on
Commit
·
ed71793
1
Parent(s):
f01bab8
UI Ux
Browse files- backend/main.py +343 -105
- static/script.js +498 -159
- static/style.css +529 -73
- templates/index.html +238 -116
backend/main.py
CHANGED
@@ -2,7 +2,7 @@ from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
|
|
2 |
from fastapi.responses import HTMLResponse, JSONResponse
|
3 |
from fastapi.staticfiles import StaticFiles
|
4 |
from fastapi.templating import Jinja2Templates
|
5 |
-
from typing import List, Optional
|
6 |
from pydantic import BaseModel
|
7 |
import os
|
8 |
import requests
|
@@ -30,14 +30,19 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
30 |
# Adjust paths to go one level up from backend to find templates/static
|
31 |
TEMPLATE_DIR = os.path.join(os.path.dirname(BASE_DIR), "templates")
|
32 |
STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
|
|
|
|
|
|
|
|
33 |
|
34 |
# --- Initialize FastAPI ---
|
35 |
-
app = FastAPI()
|
36 |
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
37 |
templates = Jinja2Templates(directory=TEMPLATE_DIR)
|
38 |
|
39 |
# --- Language mapping ---
|
40 |
LANGUAGE_MAP = {
|
|
|
41 |
"en": "English",
|
42 |
"fr": "French",
|
43 |
"es": "Spanish",
|
@@ -49,7 +54,16 @@ LANGUAGE_MAP = {
|
|
49 |
"pt": "Portuguese",
|
50 |
"tr": "Turkish",
|
51 |
"ko": "Korean",
|
52 |
-
"it": "Italian"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
}
|
54 |
|
55 |
# --- Set cache directory to a writeable location ---
|
@@ -59,19 +73,75 @@ os.environ['TRANSFORMERS_CACHE'] = '/tmp/transformers_cache'
|
|
59 |
os.environ['HF_HOME'] = '/tmp/hf_home'
|
60 |
os.environ['XDG_CACHE_HOME'] = '/tmp/cache'
|
61 |
|
62 |
-
# --- Global model
|
63 |
-
|
64 |
-
|
65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
model_initialization_attempts = 0
|
67 |
max_model_initialization_attempts = 3
|
68 |
last_initialization_attempt = 0
|
69 |
initialization_cooldown = 300 # 5 minutes cooldown between retry attempts
|
70 |
|
71 |
# --- Model initialization function ---
|
72 |
-
def initialize_model():
|
73 |
-
"""Initialize
|
74 |
-
global
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
# Check if we've exceeded maximum attempts and if enough time has passed since last attempt
|
77 |
current_time = time.time()
|
@@ -85,10 +155,10 @@ def initialize_model():
|
|
85 |
last_initialization_attempt = current_time
|
86 |
|
87 |
try:
|
88 |
-
|
|
|
89 |
|
90 |
-
|
91 |
-
model_name = "Helsinki-NLP/opus-mt-en-ar" # Much smaller English-to-Arabic model
|
92 |
|
93 |
# Check for available device - properly detect CPU/GPU
|
94 |
device = "cpu" # Default to CPU which is more reliable
|
@@ -98,7 +168,6 @@ def initialize_model():
|
|
98 |
print(f"Device set to use: {device}")
|
99 |
|
100 |
# Load the tokenizer with explicit cache directory
|
101 |
-
print(f"Loading tokenizer from {model_name}...")
|
102 |
try:
|
103 |
tokenizer = AutoTokenizer.from_pretrained(
|
104 |
model_name,
|
@@ -107,15 +176,15 @@ def initialize_model():
|
|
107 |
local_files_only=False
|
108 |
)
|
109 |
if tokenizer is None:
|
110 |
-
print("Failed to load tokenizer")
|
111 |
return False
|
112 |
-
print("Tokenizer loaded successfully")
|
|
|
113 |
except Exception as e:
|
114 |
-
print(f"Error loading tokenizer: {e}")
|
115 |
return False
|
116 |
|
117 |
# Load the model with explicit device placement
|
118 |
-
print(f"Loading model from {model_name}...")
|
119 |
try:
|
120 |
model = AutoModelForSeq2SeqLM.from_pretrained(
|
121 |
model_name,
|
@@ -125,14 +194,14 @@ def initialize_model():
|
|
125 |
)
|
126 |
# Move model to device after loading
|
127 |
model = model.to(device)
|
128 |
-
print(f"Model loaded with PyTorch and moved to {device}")
|
|
|
129 |
except Exception as e:
|
130 |
-
print(f"Error loading model: {e}")
|
131 |
-
print("Model initialization failed")
|
132 |
return False
|
133 |
|
134 |
# Create a pipeline with the loaded model and tokenizer
|
135 |
-
print("Creating translation pipeline...")
|
136 |
try:
|
137 |
# Create the pipeline with explicit model and tokenizer
|
138 |
translator = pipeline(
|
@@ -144,79 +213,175 @@ def initialize_model():
|
|
144 |
)
|
145 |
|
146 |
if translator is None:
|
147 |
-
print("Failed to create translator pipeline")
|
148 |
return False
|
149 |
|
150 |
# Test the model with a simple translation to verify it works
|
151 |
-
|
152 |
-
|
|
|
|
|
153 |
if not test_result or not isinstance(test_result, list) or len(test_result) == 0:
|
154 |
-
print("Model test failed: Invalid output format")
|
155 |
return False
|
156 |
|
|
|
|
|
157 |
# Success - reset the attempt counter
|
158 |
model_initialization_attempts = 0
|
159 |
-
print(f"Model {model_name} successfully initialized and tested")
|
160 |
return True
|
161 |
except Exception as inner_e:
|
162 |
-
print(f"Error creating translation pipeline: {inner_e}")
|
163 |
traceback.print_exc()
|
164 |
return False
|
165 |
except Exception as e:
|
166 |
-
print(f"Critical error initializing model: {e}")
|
167 |
traceback.print_exc()
|
168 |
return False
|
169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
# --- Translation Function ---
|
171 |
def translate_text(text, source_lang, target_lang):
|
172 |
"""Translate text using local model or fallback to online services."""
|
173 |
-
|
|
|
174 |
|
175 |
print(f"Translation Request - Source Lang: {source_lang}, Target Lang: {target_lang}")
|
176 |
|
177 |
-
#
|
178 |
-
|
179 |
-
success = initialize_model()
|
180 |
-
if not success:
|
181 |
-
print("Local model initialization failed, using fallback translation")
|
182 |
-
return use_fallback_translation(text, source_lang, target_lang)
|
183 |
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
|
189 |
-
#
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
|
|
|
|
|
|
|
|
|
|
197 |
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
|
|
205 |
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
220 |
return use_fallback_translation(text, source_lang, target_lang)
|
221 |
|
222 |
def culturally_adapt_arabic(text: str) -> str:
|
@@ -238,31 +403,39 @@ def culturally_adapt_arabic(text: str) -> str:
|
|
238 |
return text
|
239 |
|
240 |
# --- Function to check model status and trigger re-initialization if needed ---
|
241 |
-
def check_and_reinitialize_model():
|
242 |
"""Check if model needs to be reinitialized and do so if necessary"""
|
243 |
-
global
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
|
245 |
try:
|
246 |
# If model isn't initialized yet, try to initialize it
|
247 |
-
if not
|
248 |
-
print("Model not initialized. Attempting initialization...")
|
249 |
-
return initialize_model()
|
250 |
|
251 |
# Test the existing model with a simple translation
|
252 |
-
|
|
|
253 |
result = translator(test_text, max_length=128)
|
254 |
|
255 |
# If we got a valid result, model is working fine
|
256 |
if result and isinstance(result, list) and len(result) > 0:
|
257 |
-
print("Model check: Model is functioning correctly.")
|
258 |
return True
|
259 |
else:
|
260 |
-
print("Model check: Model returned invalid result. Reinitializing...")
|
261 |
-
return initialize_model()
|
262 |
except Exception as e:
|
263 |
-
print(f"Error checking model status: {e}")
|
264 |
print("Model may be in a bad state. Attempting reinitialization...")
|
265 |
-
return initialize_model()
|
266 |
|
267 |
def use_fallback_translation(text, source_lang, target_lang):
|
268 |
"""Use various fallback online translation services."""
|
@@ -286,7 +459,7 @@ def use_fallback_translation(text, source_lang, target_lang):
|
|
286 |
"https://libretranslate.de/translate",
|
287 |
"https://translate.argosopentech.com/translate",
|
288 |
"https://translate.fedilab.app/translate",
|
289 |
-
"https://trans.zillyhuhn.com/translate"
|
290 |
]
|
291 |
|
292 |
# Try each LibreTranslate server with increased timeout
|
@@ -302,8 +475,8 @@ def use_fallback_translation(text, source_lang, target_lang):
|
|
302 |
"target": target_lang
|
303 |
}
|
304 |
|
305 |
-
# Use a longer timeout for the request
|
306 |
-
response = requests.post(server, json=payload, headers=headers, timeout=
|
307 |
|
308 |
if response.status_code == 200:
|
309 |
result = response.json()
|
@@ -399,10 +572,13 @@ async def read_root(request: Request):
|
|
399 |
"""Serves the main HTML page."""
|
400 |
return templates.TemplateResponse("index.html", {"request": request})
|
401 |
|
|
|
|
|
|
|
|
|
|
|
402 |
@app.post("/translate/text")
|
403 |
async def translate_text_endpoint(request: TranslationRequest):
|
404 |
-
global translator, model, tokenizer
|
405 |
-
|
406 |
print("[DEBUG] /translate/text endpoint called")
|
407 |
try:
|
408 |
# Explicitly extract fields from request to ensure they exist
|
@@ -412,6 +588,13 @@ async def translate_text_endpoint(request: TranslationRequest):
|
|
412 |
|
413 |
print(f"[DEBUG] Received request: source_lang={source_lang}, target_lang={target_lang}, text={text[:50]}")
|
414 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
415 |
# Call our culturally-aware translate_text function
|
416 |
translation_result = translate_text(text, source_lang, target_lang)
|
417 |
|
@@ -424,7 +607,17 @@ async def translate_text_endpoint(request: TranslationRequest):
|
|
424 |
)
|
425 |
|
426 |
print(f"[DEBUG] Translation successful: {translation_result[:100]}...")
|
427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
428 |
|
429 |
except Exception as e:
|
430 |
print(f"Critical error in translate_text_endpoint: {str(e)}")
|
@@ -441,35 +634,80 @@ async def translate_document_endpoint(
|
|
441 |
target_lang: str = Form("ar")
|
442 |
):
|
443 |
"""Translates text extracted from an uploaded document."""
|
444 |
-
print("[DEBUG] /translate/document endpoint called
|
445 |
try:
|
446 |
# Extract text directly from the uploaded file
|
447 |
print(f"[DEBUG] Processing file: {file.filename}, Source: {source_lang}, Target: {target_lang}")
|
448 |
-
extracted_text = await extract_text_from_file(file)
|
449 |
|
450 |
-
|
451 |
-
|
452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
|
454 |
-
# Translate the extracted text
|
455 |
-
print("[DEBUG] Calling translate_text for document content...")
|
456 |
translated_text = translate_text(extracted_text, source_lang, target_lang)
|
457 |
-
|
458 |
-
|
459 |
-
|
|
|
460 |
"original_filename": file.filename,
|
461 |
-
"
|
462 |
"translated_text": translated_text
|
463 |
-
}
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
except Exception as e:
|
469 |
-
print(f"
|
470 |
traceback.print_exc()
|
471 |
-
|
472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
473 |
|
474 |
# --- Run the server (for local development) ---
|
475 |
if __name__ == "__main__":
|
|
|
2 |
from fastapi.responses import HTMLResponse, JSONResponse
|
3 |
from fastapi.staticfiles import StaticFiles
|
4 |
from fastapi.templating import Jinja2Templates
|
5 |
+
from typing import List, Optional, Dict
|
6 |
from pydantic import BaseModel
|
7 |
import os
|
8 |
import requests
|
|
|
30 |
# Adjust paths to go one level up from backend to find templates/static
|
31 |
TEMPLATE_DIR = os.path.join(os.path.dirname(BASE_DIR), "templates")
|
32 |
STATIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "static")
|
33 |
+
UPLOADS_DIR = os.path.join(os.path.dirname(BASE_DIR), "uploads")
|
34 |
+
|
35 |
+
# Ensure uploads directory exists
|
36 |
+
os.makedirs(UPLOADS_DIR, exist_ok=True)
|
37 |
|
38 |
# --- Initialize FastAPI ---
|
39 |
+
app = FastAPI(title="Tarjama Translation API")
|
40 |
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
41 |
templates = Jinja2Templates(directory=TEMPLATE_DIR)
|
42 |
|
43 |
# --- Language mapping ---
|
44 |
LANGUAGE_MAP = {
|
45 |
+
"ar": "Arabic",
|
46 |
"en": "English",
|
47 |
"fr": "French",
|
48 |
"es": "Spanish",
|
|
|
54 |
"pt": "Portuguese",
|
55 |
"tr": "Turkish",
|
56 |
"ko": "Korean",
|
57 |
+
"it": "Italian",
|
58 |
+
"nl": "Dutch",
|
59 |
+
"sv": "Swedish",
|
60 |
+
"fi": "Finnish",
|
61 |
+
"pl": "Polish",
|
62 |
+
"he": "Hebrew",
|
63 |
+
"id": "Indonesian",
|
64 |
+
"uk": "Ukrainian",
|
65 |
+
"cs": "Czech",
|
66 |
+
"auto": "Detect Language"
|
67 |
}
|
68 |
|
69 |
# --- Set cache directory to a writeable location ---
|
|
|
73 |
os.environ['HF_HOME'] = '/tmp/hf_home'
|
74 |
os.environ['XDG_CACHE_HOME'] = '/tmp/cache'
|
75 |
|
76 |
+
# --- Global model variables ---
|
77 |
+
# Store multiple translation models to support various language pairs
|
78 |
+
translation_models: Dict[str, Dict] = {
|
79 |
+
"en-ar": {
|
80 |
+
"model": None,
|
81 |
+
"tokenizer": None,
|
82 |
+
"translator": None,
|
83 |
+
"model_name": "Helsinki-NLP/opus-mt-en-ar",
|
84 |
+
},
|
85 |
+
"ar-en": {
|
86 |
+
"model": None,
|
87 |
+
"tokenizer": None,
|
88 |
+
"translator": None,
|
89 |
+
"model_name": "Helsinki-NLP/opus-mt-ar-en",
|
90 |
+
},
|
91 |
+
# Add more language pair models
|
92 |
+
"en-fr": {
|
93 |
+
"model": None,
|
94 |
+
"tokenizer": None,
|
95 |
+
"translator": None,
|
96 |
+
"model_name": "Helsinki-NLP/opus-mt-en-fr",
|
97 |
+
},
|
98 |
+
"fr-en": {
|
99 |
+
"model": None,
|
100 |
+
"tokenizer": None,
|
101 |
+
"translator": None,
|
102 |
+
"model_name": "Helsinki-NLP/opus-mt-fr-en",
|
103 |
+
},
|
104 |
+
"en-es": {
|
105 |
+
"model": None,
|
106 |
+
"tokenizer": None,
|
107 |
+
"translator": None,
|
108 |
+
"model_name": "Helsinki-NLP/opus-mt-en-es",
|
109 |
+
},
|
110 |
+
"es-en": {
|
111 |
+
"model": None,
|
112 |
+
"tokenizer": None,
|
113 |
+
"translator": None,
|
114 |
+
"model_name": "Helsinki-NLP/opus-mt-es-en",
|
115 |
+
},
|
116 |
+
"en-de": {
|
117 |
+
"model": None,
|
118 |
+
"tokenizer": None,
|
119 |
+
"translator": None,
|
120 |
+
"model_name": "Helsinki-NLP/opus-mt-en-de",
|
121 |
+
},
|
122 |
+
"de-en": {
|
123 |
+
"model": None,
|
124 |
+
"tokenizer": None,
|
125 |
+
"translator": None,
|
126 |
+
"model_name": "Helsinki-NLP/opus-mt-de-en",
|
127 |
+
},
|
128 |
+
# Can add more language pairs here as needed
|
129 |
+
}
|
130 |
+
|
131 |
model_initialization_attempts = 0
|
132 |
max_model_initialization_attempts = 3
|
133 |
last_initialization_attempt = 0
|
134 |
initialization_cooldown = 300 # 5 minutes cooldown between retry attempts
|
135 |
|
136 |
# --- Model initialization function ---
|
137 |
+
def initialize_model(language_pair: str):
|
138 |
+
"""Initialize a specific translation model and tokenizer for a language pair."""
|
139 |
+
global translation_models, model_initialization_attempts, last_initialization_attempt
|
140 |
+
|
141 |
+
# If language pair doesn't exist, return False
|
142 |
+
if language_pair not in translation_models:
|
143 |
+
print(f"Unsupported language pair: {language_pair}")
|
144 |
+
return False
|
145 |
|
146 |
# Check if we've exceeded maximum attempts and if enough time has passed since last attempt
|
147 |
current_time = time.time()
|
|
|
155 |
last_initialization_attempt = current_time
|
156 |
|
157 |
try:
|
158 |
+
model_info = translation_models[language_pair]
|
159 |
+
model_name = model_info["model_name"]
|
160 |
|
161 |
+
print(f"Initializing model and tokenizer for {language_pair} using {model_name} (attempt {model_initialization_attempts})...")
|
|
|
162 |
|
163 |
# Check for available device - properly detect CPU/GPU
|
164 |
device = "cpu" # Default to CPU which is more reliable
|
|
|
168 |
print(f"Device set to use: {device}")
|
169 |
|
170 |
# Load the tokenizer with explicit cache directory
|
|
|
171 |
try:
|
172 |
tokenizer = AutoTokenizer.from_pretrained(
|
173 |
model_name,
|
|
|
176 |
local_files_only=False
|
177 |
)
|
178 |
if tokenizer is None:
|
179 |
+
print(f"Failed to load tokenizer for {language_pair}")
|
180 |
return False
|
181 |
+
print(f"Tokenizer for {language_pair} loaded successfully")
|
182 |
+
translation_models[language_pair]["tokenizer"] = tokenizer
|
183 |
except Exception as e:
|
184 |
+
print(f"Error loading tokenizer for {language_pair}: {e}")
|
185 |
return False
|
186 |
|
187 |
# Load the model with explicit device placement
|
|
|
188 |
try:
|
189 |
model = AutoModelForSeq2SeqLM.from_pretrained(
|
190 |
model_name,
|
|
|
194 |
)
|
195 |
# Move model to device after loading
|
196 |
model = model.to(device)
|
197 |
+
print(f"Model for {language_pair} loaded with PyTorch and moved to {device}")
|
198 |
+
translation_models[language_pair]["model"] = model
|
199 |
except Exception as e:
|
200 |
+
print(f"Error loading model for {language_pair}: {e}")
|
201 |
+
print(f"Model initialization for {language_pair} failed")
|
202 |
return False
|
203 |
|
204 |
# Create a pipeline with the loaded model and tokenizer
|
|
|
205 |
try:
|
206 |
# Create the pipeline with explicit model and tokenizer
|
207 |
translator = pipeline(
|
|
|
213 |
)
|
214 |
|
215 |
if translator is None:
|
216 |
+
print(f"Failed to create translator pipeline for {language_pair}")
|
217 |
return False
|
218 |
|
219 |
# Test the model with a simple translation to verify it works
|
220 |
+
source_lang, target_lang = language_pair.split('-')
|
221 |
+
test_text = "hello world" if source_lang == "en" else "مرحبا بالعالم"
|
222 |
+
test_result = translator(test_text, max_length=128)
|
223 |
+
print(f"Model test result for {language_pair}: {test_result}")
|
224 |
if not test_result or not isinstance(test_result, list) or len(test_result) == 0:
|
225 |
+
print(f"Model test for {language_pair} failed: Invalid output format")
|
226 |
return False
|
227 |
|
228 |
+
translation_models[language_pair]["translator"] = translator
|
229 |
+
|
230 |
# Success - reset the attempt counter
|
231 |
model_initialization_attempts = 0
|
232 |
+
print(f"Model {model_name} for {language_pair} successfully initialized and tested")
|
233 |
return True
|
234 |
except Exception as inner_e:
|
235 |
+
print(f"Error creating translation pipeline for {language_pair}: {inner_e}")
|
236 |
traceback.print_exc()
|
237 |
return False
|
238 |
except Exception as e:
|
239 |
+
print(f"Critical error initializing model for {language_pair}: {e}")
|
240 |
traceback.print_exc()
|
241 |
return False
|
242 |
|
243 |
+
# --- Get appropriate language pair for translation ---
|
244 |
+
def get_language_pair(source_lang: str, target_lang: str):
|
245 |
+
"""Determine the appropriate language pair and direction for translation."""
|
246 |
+
# Handle auto-detection case (fallback to online services)
|
247 |
+
if source_lang == "auto":
|
248 |
+
return None
|
249 |
+
|
250 |
+
# Check if we have a direct model for this language pair
|
251 |
+
pair_key = f"{source_lang}-{target_lang}"
|
252 |
+
if pair_key in translation_models:
|
253 |
+
return pair_key
|
254 |
+
|
255 |
+
# No direct model available
|
256 |
+
return None
|
257 |
+
|
258 |
+
# --- Language detection function ---
|
259 |
+
def detect_language(text: str) -> str:
|
260 |
+
"""Detect the language of the input text and return the language code."""
|
261 |
+
try:
|
262 |
+
# Try to use langdetect library if available
|
263 |
+
from langdetect import detect
|
264 |
+
|
265 |
+
try:
|
266 |
+
detected_lang = detect(text)
|
267 |
+
print(f"Language detected using langdetect: {detected_lang}")
|
268 |
+
|
269 |
+
# Map langdetect specific codes to our standard codes
|
270 |
+
lang_map = {
|
271 |
+
"ar": "ar", "en": "en", "fr": "fr", "es": "es", "de": "de",
|
272 |
+
"zh-cn": "zh", "zh-tw": "zh", "ru": "ru", "ja": "ja",
|
273 |
+
"hi": "hi", "pt": "pt", "tr": "tr", "ko": "ko",
|
274 |
+
"it": "it", "nl": "nl", "sv": "sv", "fi": "fi",
|
275 |
+
"pl": "pl", "he": "he", "id": "id", "uk": "uk", "cs": "cs"
|
276 |
+
}
|
277 |
+
|
278 |
+
# Return the mapped language or default to English if not in our supported languages
|
279 |
+
return lang_map.get(detected_lang, "en")
|
280 |
+
except Exception as e:
|
281 |
+
print(f"Error with langdetect: {e}")
|
282 |
+
# Fall back to basic detection
|
283 |
+
except ImportError:
|
284 |
+
print("langdetect library not available, using basic detection")
|
285 |
+
|
286 |
+
# Basic fallback detection based on character ranges
|
287 |
+
if len(text) < 10: # Need reasonable amount of text
|
288 |
+
return "en" # Default to English for very short texts
|
289 |
+
|
290 |
+
# Count characters in different Unicode ranges
|
291 |
+
arabic_count = sum(1 for c in text if '\u0600' <= c <= '\u06FF')
|
292 |
+
chinese_count = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
|
293 |
+
japanese_count = sum(1 for c in text if '\u3040' <= c <= '\u30ff')
|
294 |
+
cyrillic_count = sum(1 for c in text if '\u0400' <= c <= '\u04FF')
|
295 |
+
hebrew_count = sum(1 for c in text if '\u0590' <= c <= '\u05FF')
|
296 |
+
|
297 |
+
# Determine ratios
|
298 |
+
text_len = len(text)
|
299 |
+
arabic_ratio = arabic_count / text_len
|
300 |
+
chinese_ratio = chinese_count / text_len
|
301 |
+
japanese_ratio = japanese_count / text_len
|
302 |
+
cyrillic_ratio = cyrillic_count / text_len
|
303 |
+
hebrew_ratio = hebrew_count / text_len
|
304 |
+
|
305 |
+
# Make decision based on highest ratio
|
306 |
+
if arabic_ratio > 0.3:
|
307 |
+
return "ar"
|
308 |
+
elif chinese_ratio > 0.3:
|
309 |
+
return "zh"
|
310 |
+
elif japanese_ratio > 0.3:
|
311 |
+
return "ja"
|
312 |
+
elif cyrillic_ratio > 0.3:
|
313 |
+
return "ru"
|
314 |
+
elif hebrew_ratio > 0.3:
|
315 |
+
return "he"
|
316 |
+
|
317 |
+
# Default to English for Latin scripts (could be any European language)
|
318 |
+
return "en"
|
319 |
+
|
320 |
# --- Translation Function ---
|
321 |
def translate_text(text, source_lang, target_lang):
|
322 |
"""Translate text using local model or fallback to online services."""
|
323 |
+
if not text:
|
324 |
+
return ""
|
325 |
|
326 |
print(f"Translation Request - Source Lang: {source_lang}, Target Lang: {target_lang}")
|
327 |
|
328 |
+
# Get the appropriate language pair for local translation
|
329 |
+
language_pair = get_language_pair(source_lang, target_lang)
|
|
|
|
|
|
|
|
|
330 |
|
331 |
+
# If we have a supported local model for this language pair
|
332 |
+
if language_pair and language_pair in translation_models:
|
333 |
+
model_info = translation_models[language_pair]
|
334 |
+
translator = model_info["translator"]
|
335 |
|
336 |
+
# Check if model is initialized, if not try to initialize it
|
337 |
+
if not translator:
|
338 |
+
success = initialize_model(language_pair)
|
339 |
+
if not success:
|
340 |
+
print(f"Local model initialization for {language_pair} failed, using fallback translation")
|
341 |
+
return use_fallback_translation(text, source_lang, target_lang)
|
342 |
+
# Get the translator after initialization
|
343 |
+
translator = translation_models[language_pair]["translator"]
|
344 |
+
|
345 |
+
try:
|
346 |
+
# Ensure only the raw text is sent to the model
|
347 |
+
text_to_translate = text
|
348 |
+
print(f"Translating text with local model (first 50 chars): {text_to_translate[:50]}...")
|
349 |
|
350 |
+
# Use a more reliable timeout approach with concurrent.futures
|
351 |
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
352 |
+
future = executor.submit(
|
353 |
+
lambda: translator(
|
354 |
+
text_to_translate,
|
355 |
+
max_length=768
|
356 |
+
)[0]["translation_text"]
|
357 |
+
)
|
358 |
|
359 |
+
try:
|
360 |
+
# Set a reasonable timeout
|
361 |
+
result = future.result(timeout=15)
|
362 |
+
|
363 |
+
# Post-process the result for cultural adaptation if needed
|
364 |
+
if target_lang == "ar":
|
365 |
+
result = culturally_adapt_arabic(result)
|
366 |
+
|
367 |
+
print(f"Translation successful (first 50 chars): {result[:50]}...")
|
368 |
+
return result
|
369 |
+
except concurrent.futures.TimeoutError:
|
370 |
+
print(f"Model inference timed out after 15 seconds, falling back to online translation")
|
371 |
+
return use_fallback_translation(text, source_lang, target_lang)
|
372 |
+
except Exception as e:
|
373 |
+
print(f"Error during model inference: {e}")
|
374 |
+
# If the model failed during inference, try to re-initialize it for next time
|
375 |
+
# but use fallback for this request
|
376 |
+
initialize_model(language_pair)
|
377 |
+
return use_fallback_translation(text, source_lang, target_lang)
|
378 |
+
except Exception as e:
|
379 |
+
print(f"Error using local model for {language_pair}: {e}")
|
380 |
+
traceback.print_exc()
|
381 |
+
return use_fallback_translation(text, source_lang, target_lang)
|
382 |
+
else:
|
383 |
+
# No local model for this language pair, use online services
|
384 |
+
print(f"No local model for {source_lang} to {target_lang}, using fallback translation")
|
385 |
return use_fallback_translation(text, source_lang, target_lang)
|
386 |
|
387 |
def culturally_adapt_arabic(text: str) -> str:
|
|
|
403 |
return text
|
404 |
|
405 |
# --- Function to check model status and trigger re-initialization if needed ---
|
406 |
+
def check_and_reinitialize_model(language_pair: str):
|
407 |
"""Check if model needs to be reinitialized and do so if necessary"""
|
408 |
+
global translation_models
|
409 |
+
|
410 |
+
if language_pair not in translation_models:
|
411 |
+
print(f"Unsupported language pair: {language_pair}")
|
412 |
+
return False
|
413 |
+
|
414 |
+
model_info = translation_models[language_pair]
|
415 |
+
translator = model_info["translator"]
|
416 |
|
417 |
try:
|
418 |
# If model isn't initialized yet, try to initialize it
|
419 |
+
if not translator:
|
420 |
+
print(f"Model for {language_pair} not initialized. Attempting initialization...")
|
421 |
+
return initialize_model(language_pair)
|
422 |
|
423 |
# Test the existing model with a simple translation
|
424 |
+
source_lang, target_lang = language_pair.split('-')
|
425 |
+
test_text = "hello" if source_lang == "en" else "مرحبا"
|
426 |
result = translator(test_text, max_length=128)
|
427 |
|
428 |
# If we got a valid result, model is working fine
|
429 |
if result and isinstance(result, list) and len(result) > 0:
|
430 |
+
print(f"Model check for {language_pair}: Model is functioning correctly.")
|
431 |
return True
|
432 |
else:
|
433 |
+
print(f"Model check for {language_pair}: Model returned invalid result. Reinitializing...")
|
434 |
+
return initialize_model(language_pair)
|
435 |
except Exception as e:
|
436 |
+
print(f"Error checking model status for {language_pair}: {e}")
|
437 |
print("Model may be in a bad state. Attempting reinitialization...")
|
438 |
+
return initialize_model(language_pair)
|
439 |
|
440 |
def use_fallback_translation(text, source_lang, target_lang):
|
441 |
"""Use various fallback online translation services."""
|
|
|
459 |
"https://libretranslate.de/translate",
|
460 |
"https://translate.argosopentech.com/translate",
|
461 |
"https://translate.fedilab.app/translate",
|
462 |
+
"https://trans.zillyhuhn.com/translate"
|
463 |
]
|
464 |
|
465 |
# Try each LibreTranslate server with increased timeout
|
|
|
475 |
"target": target_lang
|
476 |
}
|
477 |
|
478 |
+
# Use a longer timeout for the request
|
479 |
+
response = requests.post(server, json=payload, headers=headers, timeout=10)
|
480 |
|
481 |
if response.status_code == 200:
|
482 |
result = response.json()
|
|
|
572 |
"""Serves the main HTML page."""
|
573 |
return templates.TemplateResponse("index.html", {"request": request})
|
574 |
|
575 |
+
@app.get("/api/languages")
|
576 |
+
async def get_languages():
|
577 |
+
"""Return the list of supported languages."""
|
578 |
+
return {"languages": LANGUAGE_MAP}
|
579 |
+
|
580 |
@app.post("/translate/text")
|
581 |
async def translate_text_endpoint(request: TranslationRequest):
|
|
|
|
|
582 |
print("[DEBUG] /translate/text endpoint called")
|
583 |
try:
|
584 |
# Explicitly extract fields from request to ensure they exist
|
|
|
588 |
|
589 |
print(f"[DEBUG] Received request: source_lang={source_lang}, target_lang={target_lang}, text={text[:50]}")
|
590 |
|
591 |
+
# Handle automatic language detection
|
592 |
+
detected_source_lang = None
|
593 |
+
if source_lang == "auto":
|
594 |
+
detected_source_lang = detect_language(text)
|
595 |
+
print(f"[DEBUG] Detected language: {detected_source_lang}")
|
596 |
+
source_lang = detected_source_lang
|
597 |
+
|
598 |
# Call our culturally-aware translate_text function
|
599 |
translation_result = translate_text(text, source_lang, target_lang)
|
600 |
|
|
|
607 |
)
|
608 |
|
609 |
print(f"[DEBUG] Translation successful: {translation_result[:100]}...")
|
610 |
+
|
611 |
+
# Include detected language in response if auto-detection was used
|
612 |
+
response_data = {
|
613 |
+
"success": True,
|
614 |
+
"translated_text": translation_result
|
615 |
+
}
|
616 |
+
|
617 |
+
if detected_source_lang:
|
618 |
+
response_data["detected_source_lang"] = detected_source_lang
|
619 |
+
|
620 |
+
return response_data
|
621 |
|
622 |
except Exception as e:
|
623 |
print(f"Critical error in translate_text_endpoint: {str(e)}")
|
|
|
634 |
target_lang: str = Form("ar")
|
635 |
):
|
636 |
"""Translates text extracted from an uploaded document."""
|
637 |
+
print("[DEBUG] /translate/document endpoint called")
|
638 |
try:
|
639 |
# Extract text directly from the uploaded file
|
640 |
print(f"[DEBUG] Processing file: {file.filename}, Source: {source_lang}, Target: {target_lang}")
|
|
|
641 |
|
642 |
+
# Extract text from document
|
643 |
+
extracted_text = await extract_text_from_file(file)
|
644 |
+
if not extracted_text or extracted_text.strip() == "":
|
645 |
+
return JSONResponse(
|
646 |
+
status_code=400,
|
647 |
+
content={"success": False, "error": "Could not extract text from document"}
|
648 |
+
)
|
649 |
+
|
650 |
+
# Handle automatic language detection
|
651 |
+
detected_source_lang = None
|
652 |
+
if source_lang == "auto":
|
653 |
+
detected_source_lang = detect_language(extracted_text)
|
654 |
+
print(f"[DEBUG] Detected document language: {detected_source_lang}")
|
655 |
+
source_lang = detected_source_lang
|
656 |
|
657 |
+
# Translate the extracted text
|
|
|
658 |
translated_text = translate_text(extracted_text, source_lang, target_lang)
|
659 |
+
|
660 |
+
# Prepare response
|
661 |
+
response = {
|
662 |
+
"success": True,
|
663 |
"original_filename": file.filename,
|
664 |
+
"original_text": extracted_text[:2000] + ("..." if len(extracted_text) > 2000 else ""),
|
665 |
"translated_text": translated_text
|
666 |
+
}
|
667 |
+
|
668 |
+
# Include detected language in response if auto-detection was used
|
669 |
+
if detected_source_lang:
|
670 |
+
response["detected_source_lang"] = detected_source_lang
|
671 |
+
|
672 |
+
return response
|
673 |
+
|
674 |
+
except HTTPException as e:
|
675 |
+
# Re-raise HTTP exceptions
|
676 |
+
raise e
|
677 |
except Exception as e:
|
678 |
+
print(f"Error in document translation: {str(e)}")
|
679 |
traceback.print_exc()
|
680 |
+
return JSONResponse(
|
681 |
+
status_code=500,
|
682 |
+
content={"success": False, "error": f"Document translation failed: {str(e)}"}
|
683 |
+
)
|
684 |
+
|
685 |
+
# Initialize models during startup
|
686 |
+
@app.on_event("startup")
|
687 |
+
async def startup_event():
|
688 |
+
"""Initialize models during application startup."""
|
689 |
+
# Initial model loading for the most common language pairs
|
690 |
+
# We load them asynchronously to not block the startup
|
691 |
+
try:
|
692 |
+
# Try to initialize English-to-Arabic model
|
693 |
+
initialize_model("en-ar")
|
694 |
+
except Exception as e:
|
695 |
+
print(f"Error initializing en-ar model at startup: {e}")
|
696 |
+
|
697 |
+
try:
|
698 |
+
# Try to initialize Arabic-to-English model
|
699 |
+
initialize_model("ar-en")
|
700 |
+
except Exception as e:
|
701 |
+
print(f"Error initializing ar-en model at startup: {e}")
|
702 |
+
|
703 |
+
# Initialize additional models for common language pairs
|
704 |
+
# These will be initialized in the background without blocking startup
|
705 |
+
common_pairs = ["en-fr", "fr-en", "en-es", "es-en"]
|
706 |
+
for pair in common_pairs:
|
707 |
+
try:
|
708 |
+
initialize_model(pair)
|
709 |
+
except Exception as e:
|
710 |
+
print(f"Error initializing {pair} model at startup: {e}")
|
711 |
|
712 |
# --- Run the server (for local development) ---
|
713 |
if __name__ == "__main__":
|
static/script.js
CHANGED
@@ -1,193 +1,532 @@
|
|
1 |
// Wait for the DOM to be fully loaded before attaching event handlers
|
2 |
window.onload = function() {
|
3 |
-
console.log('Window fully loaded, initializing
|
4 |
-
|
5 |
-
// Get
|
6 |
-
const
|
7 |
-
const
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
const
|
13 |
-
const
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
}
|
23 |
-
return;
|
24 |
}
|
25 |
|
26 |
-
//
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
31 |
|
32 |
-
//
|
33 |
-
if (
|
34 |
-
|
35 |
-
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
|
46 |
-
|
47 |
-
|
48 |
if (!text) {
|
49 |
-
|
50 |
return;
|
51 |
}
|
52 |
|
53 |
-
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
return;
|
57 |
}
|
58 |
|
59 |
-
|
60 |
-
|
|
|
61 |
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
text: text,
|
65 |
-
source_lang:
|
66 |
-
target_lang:
|
67 |
-
}
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
}
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
data = JSON.parse(responseText);
|
86 |
-
} catch (error) {
|
87 |
-
console.error("Failed to parse JSON:", responseText);
|
88 |
-
throw new Error('Invalid response format from server');
|
89 |
-
}
|
90 |
|
91 |
-
|
92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
}
|
|
|
|
|
|
|
|
|
|
|
94 |
|
95 |
-
//
|
96 |
-
if (
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
}
|
101 |
-
.catch(error => {
|
102 |
-
showError(error.message || 'Translation failed');
|
103 |
-
console.error('Error:', error);
|
104 |
-
})
|
105 |
-
.finally(() => {
|
106 |
-
if (textLoadingElement) textLoadingElement.style.display = 'none';
|
107 |
-
});
|
108 |
-
|
109 |
-
} catch (e) {
|
110 |
-
console.error('FATAL: Error processing form data or submitting:', e);
|
111 |
-
showError('Internal error processing form submission. Check console.');
|
112 |
-
}
|
113 |
-
// --- End FormData Approach ---
|
114 |
-
});
|
115 |
-
|
116 |
-
// Document translation handler
|
117 |
-
if (docTranslationForm) {
|
118 |
-
console.log('Document translation form found on load');
|
119 |
-
docTranslationForm.addEventListener('submit', function(event) {
|
120 |
-
event.preventDefault();
|
121 |
-
console.log('Document translation form submitted');
|
122 |
-
|
123 |
-
// Clear previous results and errors
|
124 |
-
document.querySelectorAll('#doc-result, #error-message').forEach(el => {
|
125 |
-
if (el) el.style.display = 'none';
|
126 |
-
});
|
127 |
|
128 |
-
//
|
129 |
-
|
130 |
-
|
|
|
|
|
131 |
|
132 |
-
if (
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
}
|
|
|
|
|
|
|
|
|
136 |
|
137 |
-
|
138 |
-
|
|
|
139 |
|
140 |
-
//
|
141 |
-
|
|
|
|
|
142 |
|
143 |
-
//
|
144 |
-
|
145 |
-
|
146 |
-
body: formData
|
147 |
-
})
|
148 |
-
.then(function(response) {
|
149 |
-
if (!response.ok) {
|
150 |
-
throw new Error(`Server returned ${response.status}`);
|
151 |
-
}
|
152 |
-
return response.json();
|
153 |
-
})
|
154 |
-
.then(function(data) {
|
155 |
-
if (!data.translated_text) {
|
156 |
-
throw new Error('No translation returned');
|
157 |
-
}
|
158 |
|
159 |
-
//
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
if (resultBox && outputEl) {
|
166 |
-
if (filenameEl) filenameEl.textContent = data.original_filename || 'N/A';
|
167 |
-
if (sourceLangEl) sourceLangEl.textContent = data.detected_source_lang || 'N/A';
|
168 |
-
outputEl.textContent = data.translated_text;
|
169 |
-
resultBox.style.display = 'block';
|
170 |
-
}
|
171 |
-
})
|
172 |
-
.catch(function(error) {
|
173 |
-
showError(error.message);
|
174 |
-
})
|
175 |
-
.finally(function() {
|
176 |
-
if (loadingIndicator) {
|
177 |
-
loadingIndicator.style.display = 'none';
|
178 |
}
|
179 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
});
|
181 |
-
} else {
|
182 |
-
console.error('Document translation form not found on load!');
|
183 |
}
|
184 |
|
185 |
-
// Helper function to
|
186 |
-
function
|
187 |
-
|
188 |
-
|
189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
190 |
}
|
191 |
-
console.error('Error displayed:', message);
|
192 |
}
|
|
|
|
|
|
|
193 |
};
|
|
|
1 |
// Wait for the DOM to be fully loaded before attaching event handlers
|
2 |
window.onload = function() {
|
3 |
+
console.log('Window fully loaded, initializing Tarjama app');
|
4 |
+
|
5 |
+
// Get navigation elements
|
6 |
+
const textTabLink = document.querySelector('nav ul li a[href="#text-translation"]');
|
7 |
+
const docTabLink = document.querySelector('nav ul li a[href="#document-translation"]');
|
8 |
+
const textSection = document.getElementById('text-translation');
|
9 |
+
const docSection = document.getElementById('document-translation');
|
10 |
+
|
11 |
+
// Get form elements
|
12 |
+
const textTranslationForm = document.getElementById('text-translation-form');
|
13 |
+
const docTranslationForm = document.getElementById('doc-translation-form');
|
14 |
+
|
15 |
+
// UI elements
|
16 |
+
const textInput = document.getElementById('text-input');
|
17 |
+
const textResult = document.getElementById('text-result');
|
18 |
+
const docResult = document.getElementById('document-translation');
|
19 |
+
const textOutput = document.getElementById('text-output');
|
20 |
+
const docOutput = document.getElementById('doc-output');
|
21 |
+
const docInputText = document.getElementById('doc-input-text');
|
22 |
+
const textLoadingIndicator = document.getElementById('text-loading');
|
23 |
+
const docLoadingIndicator = document.getElementById('doc-loading');
|
24 |
+
const errorMessageElement = document.getElementById('error-message');
|
25 |
+
const notificationElement = document.getElementById('notification');
|
26 |
+
const charCountElement = document.getElementById('char-count');
|
27 |
+
const fileNameDisplay = document.getElementById('file-name-display');
|
28 |
+
const docFilename = document.getElementById('doc-filename');
|
29 |
+
const docSourceLang = document.getElementById('doc-source-lang');
|
30 |
+
|
31 |
+
// Language selectors
|
32 |
+
const sourceLangText = document.getElementById('source-lang-text');
|
33 |
+
const targetLangText = document.getElementById('target-lang-text');
|
34 |
+
const sourceLangDoc = document.getElementById('source-lang-doc');
|
35 |
+
const targetLangDoc = document.getElementById('target-lang-doc');
|
36 |
+
|
37 |
+
// Get quick phrases elements
|
38 |
+
const quickPhrasesContainer = document.getElementById('quick-phrases');
|
39 |
+
const quickPhraseButtons = document.querySelectorAll('.quick-phrase');
|
40 |
+
|
41 |
+
// Control buttons
|
42 |
+
const swapLangBtn = document.getElementById('swap-lang-btn');
|
43 |
+
const copyTextBtn = document.getElementById('copy-text-btn');
|
44 |
+
const clearTextBtn = document.getElementById('clear-text-btn');
|
45 |
+
|
46 |
+
// RTL language handling - list of languages that use RTL
|
47 |
+
const rtlLanguages = ['ar', 'he'];
|
48 |
+
|
49 |
+
// Tab navigation
|
50 |
+
if (textTabLink && docTabLink && textSection && docSection) {
|
51 |
+
textTabLink.addEventListener('click', function(e) {
|
52 |
+
e.preventDefault();
|
53 |
+
docSection.style.display = 'none';
|
54 |
+
textSection.style.display = 'block';
|
55 |
+
textTabLink.parentElement.classList.add('active');
|
56 |
+
docTabLink.parentElement.classList.remove('active');
|
57 |
+
});
|
58 |
+
|
59 |
+
docTabLink.addEventListener('click', function(e) {
|
60 |
+
e.preventDefault();
|
61 |
+
textSection.style.display = 'none';
|
62 |
+
docSection.style.display = 'block';
|
63 |
+
docTabLink.parentElement.classList.add('active');
|
64 |
+
textTabLink.parentElement.classList.remove('active');
|
65 |
+
});
|
66 |
+
}
|
67 |
+
|
68 |
+
// Character count
|
69 |
+
if (textInput && charCountElement) {
|
70 |
+
textInput.addEventListener('input', function() {
|
71 |
+
const charCount = textInput.value.length;
|
72 |
+
charCountElement.textContent = `${charCount}`;
|
73 |
+
|
74 |
+
// Add warning class if approaching or exceeding character limit
|
75 |
+
if (charCount > 3000) {
|
76 |
+
charCountElement.className = 'char-count-warning';
|
77 |
+
} else if (charCount > 2000) {
|
78 |
+
charCountElement.className = 'char-count-approaching';
|
79 |
+
} else {
|
80 |
+
charCountElement.className = '';
|
81 |
+
}
|
82 |
+
});
|
83 |
+
}
|
84 |
+
|
85 |
+
// Quick phrases implementation
|
86 |
+
if (quickPhraseButtons && quickPhraseButtons.length > 0) {
|
87 |
+
quickPhraseButtons.forEach(button => {
|
88 |
+
button.addEventListener('click', function(e) {
|
89 |
+
e.preventDefault();
|
90 |
+
const phrase = this.getAttribute('data-phrase');
|
91 |
+
|
92 |
+
if (phrase && textInput) {
|
93 |
+
// Insert the phrase at cursor position, or append to end
|
94 |
+
if (typeof textInput.selectionStart === 'number') {
|
95 |
+
const startPos = textInput.selectionStart;
|
96 |
+
const endPos = textInput.selectionEnd;
|
97 |
+
const currentValue = textInput.value;
|
98 |
+
const spaceChar = currentValue && currentValue[startPos - 1] !== ' ' ? ' ' : '';
|
99 |
+
|
100 |
+
// Insert phrase at cursor position with space if needed
|
101 |
+
textInput.value = currentValue.substring(0, startPos) +
|
102 |
+
spaceChar + phrase +
|
103 |
+
currentValue.substring(endPos);
|
104 |
+
|
105 |
+
// Move cursor after inserted phrase
|
106 |
+
textInput.selectionStart = startPos + phrase.length + spaceChar.length;
|
107 |
+
textInput.selectionEnd = textInput.selectionStart;
|
108 |
+
} else {
|
109 |
+
// Fallback for browsers that don't support selection
|
110 |
+
const currentValue = textInput.value;
|
111 |
+
const spaceChar = currentValue && currentValue[currentValue.length - 1] !== ' ' ? ' ' : '';
|
112 |
+
textInput.value += spaceChar + phrase;
|
113 |
+
}
|
114 |
+
|
115 |
+
// Trigger input event to update character count
|
116 |
+
const inputEvent = new Event('input', { bubbles: true });
|
117 |
+
textInput.dispatchEvent(inputEvent);
|
118 |
+
|
119 |
+
// Focus back on the input
|
120 |
+
textInput.focus();
|
121 |
+
}
|
122 |
+
});
|
123 |
+
});
|
124 |
+
}
|
125 |
+
|
126 |
+
// Language swap functionality
|
127 |
+
if (swapLangBtn && sourceLangText && targetLangText) {
|
128 |
+
swapLangBtn.addEventListener('click', function(e) {
|
129 |
+
e.preventDefault();
|
130 |
+
|
131 |
+
// Don't swap if source is "auto" (language detection)
|
132 |
+
if (sourceLangText.value === 'auto') {
|
133 |
+
showNotification('Cannot swap when source language is set to auto-detect.');
|
134 |
+
return;
|
135 |
+
}
|
136 |
+
|
137 |
+
// Store the current values
|
138 |
+
const sourceValue = sourceLangText.value;
|
139 |
+
const targetValue = targetLangText.value;
|
140 |
+
|
141 |
+
// Swap the values
|
142 |
+
sourceLangText.value = targetValue;
|
143 |
+
targetLangText.value = sourceValue;
|
144 |
+
|
145 |
+
// If we have translated text, trigger a new translation in the opposite direction
|
146 |
+
if (textOutput.textContent.trim() !== '') {
|
147 |
+
// Update the input with the current output
|
148 |
+
textInput.value = textOutput.textContent;
|
149 |
+
|
150 |
+
// Update the character count
|
151 |
+
const inputEvent = new Event('input', { bubbles: true });
|
152 |
+
textInput.dispatchEvent(inputEvent);
|
153 |
+
|
154 |
+
// Trigger translation
|
155 |
+
const clickEvent = new Event('click');
|
156 |
+
document.querySelector('#translate-text-btn').dispatchEvent(clickEvent);
|
157 |
+
}
|
158 |
+
|
159 |
+
// Apply RTL styling as needed
|
160 |
+
applyRtlStyling(sourceLangText.value, textInput);
|
161 |
+
applyRtlStyling(targetLangText.value, textOutput);
|
162 |
+
});
|
163 |
+
}
|
164 |
+
|
165 |
+
// Apply RTL styling based on language
|
166 |
+
function applyRtlStyling(langCode, element) {
|
167 |
+
if (element) {
|
168 |
+
if (rtlLanguages.includes(langCode)) {
|
169 |
+
element.style.direction = 'rtl';
|
170 |
+
element.style.textAlign = 'right';
|
171 |
+
} else {
|
172 |
+
element.style.direction = 'ltr';
|
173 |
+
element.style.textAlign = 'left';
|
174 |
+
}
|
175 |
}
|
|
|
176 |
}
|
177 |
|
178 |
+
// Handle language change for proper text direction
|
179 |
+
function handleLanguageChange() {
|
180 |
+
// Set text direction based on selected source language
|
181 |
+
if (sourceLangText && textInput) {
|
182 |
+
applyRtlStyling(sourceLangText.value, textInput);
|
183 |
+
}
|
184 |
|
185 |
+
// Set text direction based on selected target language
|
186 |
+
if (targetLangText && textOutput) {
|
187 |
+
applyRtlStyling(targetLangText.value, textOutput);
|
188 |
+
}
|
189 |
|
190 |
+
if (sourceLangDoc && docInputText) {
|
191 |
+
applyRtlStyling(sourceLangDoc.value, docInputText);
|
192 |
+
}
|
193 |
+
|
194 |
+
if (targetLangDoc && docOutput) {
|
195 |
+
applyRtlStyling(targetLangDoc.value, docOutput);
|
196 |
+
}
|
197 |
+
}
|
198 |
+
|
199 |
+
// Add event listeners for language changes
|
200 |
+
if (sourceLangText) sourceLangText.addEventListener('change', handleLanguageChange);
|
201 |
+
if (targetLangText) targetLangText.addEventListener('change', handleLanguageChange);
|
202 |
+
if (sourceLangDoc) sourceLangDoc.addEventListener('change', handleLanguageChange);
|
203 |
+
if (targetLangDoc) targetLangDoc.addEventListener('change', handleLanguageChange);
|
204 |
+
|
205 |
+
// Copy translation to clipboard functionality
|
206 |
+
if (copyTextBtn) {
|
207 |
+
copyTextBtn.addEventListener('click', function() {
|
208 |
+
if (textOutput && textOutput.textContent.trim() !== '') {
|
209 |
+
navigator.clipboard.writeText(textOutput.textContent)
|
210 |
+
.then(() => {
|
211 |
+
showNotification('Translation copied to clipboard!');
|
212 |
+
})
|
213 |
+
.catch(err => {
|
214 |
+
console.error('Error copying text: ', err);
|
215 |
+
showNotification('Failed to copy text. Please try again.');
|
216 |
+
});
|
217 |
+
}
|
218 |
+
});
|
219 |
+
}
|
220 |
+
|
221 |
+
// Clear text functionality
|
222 |
+
if (clearTextBtn) {
|
223 |
+
clearTextBtn.addEventListener('click', function() {
|
224 |
+
if (textInput) {
|
225 |
+
textInput.value = '';
|
226 |
+
textOutput.textContent = '';
|
227 |
+
|
228 |
+
// Update character count
|
229 |
+
const inputEvent = new Event('input', { bubbles: true });
|
230 |
+
textInput.dispatchEvent(inputEvent);
|
231 |
+
|
232 |
+
// Focus back on the input
|
233 |
+
textInput.focus();
|
234 |
+
}
|
235 |
+
});
|
236 |
+
}
|
237 |
+
|
238 |
+
// Text translation form submission
|
239 |
+
if (textTranslationForm) {
|
240 |
+
textTranslationForm.addEventListener('submit', function(e) {
|
241 |
+
e.preventDefault();
|
242 |
|
243 |
+
const text = textInput.value.trim();
|
|
|
244 |
if (!text) {
|
245 |
+
showNotification('Please enter text to translate.');
|
246 |
return;
|
247 |
}
|
248 |
|
249 |
+
const sourceLang = sourceLangText.value;
|
250 |
+
const targetLang = targetLangText.value;
|
251 |
+
|
252 |
+
translateText(text, sourceLang, targetLang);
|
253 |
+
});
|
254 |
+
}
|
255 |
+
|
256 |
+
// Document translation form submission
|
257 |
+
if (docTranslationForm) {
|
258 |
+
docTranslationForm.addEventListener('submit', function(e) {
|
259 |
+
e.preventDefault();
|
260 |
+
|
261 |
+
const fileInput = document.getElementById('doc-input');
|
262 |
+
if (!fileInput.files || fileInput.files.length === 0) {
|
263 |
+
showNotification('Please select a document to translate.');
|
264 |
return;
|
265 |
}
|
266 |
|
267 |
+
const file = fileInput.files[0];
|
268 |
+
const sourceLang = sourceLangDoc.value;
|
269 |
+
const targetLang = targetLangDoc.value;
|
270 |
|
271 |
+
translateDocument(file, sourceLang, targetLang);
|
272 |
+
});
|
273 |
+
}
|
274 |
+
|
275 |
+
// File drag and drop
|
276 |
+
const dropZone = document.getElementById('drop-zone');
|
277 |
+
const fileInput = document.getElementById('doc-input');
|
278 |
+
|
279 |
+
if (dropZone && fileInput) {
|
280 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
281 |
+
dropZone.addEventListener(eventName, preventDefaults, false);
|
282 |
+
});
|
283 |
+
|
284 |
+
function preventDefaults(e) {
|
285 |
+
e.preventDefault();
|
286 |
+
e.stopPropagation();
|
287 |
+
}
|
288 |
+
|
289 |
+
['dragenter', 'dragover'].forEach(eventName => {
|
290 |
+
dropZone.addEventListener(eventName, highlight, false);
|
291 |
+
});
|
292 |
+
|
293 |
+
['dragleave', 'drop'].forEach(eventName => {
|
294 |
+
dropZone.addEventListener(eventName, unhighlight, false);
|
295 |
+
});
|
296 |
+
|
297 |
+
function highlight() {
|
298 |
+
dropZone.classList.add('highlight');
|
299 |
+
}
|
300 |
+
|
301 |
+
function unhighlight() {
|
302 |
+
dropZone.classList.remove('highlight');
|
303 |
+
}
|
304 |
+
|
305 |
+
dropZone.addEventListener('drop', handleDrop, false);
|
306 |
+
|
307 |
+
function handleDrop(e) {
|
308 |
+
const dt = e.dataTransfer;
|
309 |
+
const files = dt.files;
|
310 |
+
|
311 |
+
if (files && files.length > 0) {
|
312 |
+
fileInput.files = files;
|
313 |
+
const fileName = files[0].name;
|
314 |
+
fileNameDisplay.textContent = fileName;
|
315 |
+
fileNameDisplay.style.display = 'block';
|
316 |
+
docFilename.value = fileName;
|
317 |
+
}
|
318 |
+
}
|
319 |
+
|
320 |
+
// Handle file selection through the input
|
321 |
+
fileInput.addEventListener('change', function() {
|
322 |
+
if (this.files && this.files.length > 0) {
|
323 |
+
const fileName = this.files[0].name;
|
324 |
+
fileNameDisplay.textContent = fileName;
|
325 |
+
fileNameDisplay.style.display = 'block';
|
326 |
+
docFilename.value = fileName;
|
327 |
+
} else {
|
328 |
+
fileNameDisplay.textContent = '';
|
329 |
+
fileNameDisplay.style.display = 'none';
|
330 |
+
docFilename.value = '';
|
331 |
+
}
|
332 |
+
});
|
333 |
+
}
|
334 |
+
|
335 |
+
// Text translation function
|
336 |
+
function translateText(text, sourceLang, targetLang) {
|
337 |
+
if (textLoadingIndicator) textLoadingIndicator.style.display = 'block';
|
338 |
+
if (errorMessageElement) errorMessageElement.style.display = 'none';
|
339 |
+
|
340 |
+
// Call the API
|
341 |
+
fetch('/translate/text', {
|
342 |
+
method: 'POST',
|
343 |
+
headers: {
|
344 |
+
'Content-Type': 'application/json',
|
345 |
+
},
|
346 |
+
body: JSON.stringify({
|
347 |
text: text,
|
348 |
+
source_lang: sourceLang,
|
349 |
+
target_lang: targetLang
|
350 |
+
}),
|
351 |
+
})
|
352 |
+
.then(response => {
|
353 |
+
if (!response.ok) {
|
354 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
355 |
+
}
|
356 |
+
return response.json();
|
357 |
+
})
|
358 |
+
.then(data => {
|
359 |
+
if (textLoadingIndicator) textLoadingIndicator.style.display = 'none';
|
360 |
+
|
361 |
+
if (data.success === false) {
|
362 |
+
throw new Error(data.error || 'Translation failed with an unknown error');
|
363 |
+
}
|
364 |
+
|
365 |
+
// Handle language detection result if present
|
366 |
+
if (data.detected_source_lang && sourceLang === 'auto') {
|
367 |
+
showNotification(`Detected language: ${getLanguageName(data.detected_source_lang)}`);
|
|
|
|
|
|
|
|
|
|
|
368 |
|
369 |
+
// Optionally update the source language dropdown to show the detected language
|
370 |
+
if (sourceLangText && data.detected_source_lang) {
|
371 |
+
// Just for UI feedback - no need to change the actual value since
|
372 |
+
// we want to keep 'auto' selected for future translations
|
373 |
+
const detectedOption = Array.from(sourceLangText.options).find(
|
374 |
+
option => option.value === data.detected_source_lang
|
375 |
+
);
|
376 |
+
|
377 |
+
if (detectedOption) {
|
378 |
+
// Visual indication of detected language
|
379 |
+
sourceLangText.parentElement.setAttribute('data-detected',
|
380 |
+
`Detected: ${detectedOption.text}`);
|
381 |
+
}
|
382 |
}
|
383 |
+
}
|
384 |
+
|
385 |
+
// Show translation result
|
386 |
+
if (textOutput) {
|
387 |
+
textOutput.textContent = data.translated_text;
|
388 |
|
389 |
+
// Enable copy button
|
390 |
+
if (copyTextBtn) copyTextBtn.disabled = false;
|
391 |
+
|
392 |
+
// Apply RTL styling based on target language
|
393 |
+
applyRtlStyling(targetLang, textOutput);
|
394 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
395 |
|
396 |
+
// Show the text result container if it was hidden
|
397 |
+
if (textResult) textResult.style.display = 'block';
|
398 |
+
})
|
399 |
+
.catch(error => {
|
400 |
+
console.error('Error during translation:', error);
|
401 |
|
402 |
+
if (textLoadingIndicator) textLoadingIndicator.style.display = 'none';
|
403 |
+
|
404 |
+
// Show error message
|
405 |
+
if (errorMessageElement) {
|
406 |
+
errorMessageElement.style.display = 'block';
|
407 |
+
errorMessageElement.textContent = `Translation error: ${error.message}`;
|
408 |
+
}
|
409 |
+
});
|
410 |
+
}
|
411 |
+
|
412 |
+
// Document translation function
|
413 |
+
function translateDocument(file, sourceLang, targetLang) {
|
414 |
+
if (docLoadingIndicator) docLoadingIndicator.style.display = 'block';
|
415 |
+
if (errorMessageElement) errorMessageElement.style.display = 'none';
|
416 |
+
|
417 |
+
const formData = new FormData();
|
418 |
+
formData.append('file', file);
|
419 |
+
formData.append('source_lang', sourceLang);
|
420 |
+
formData.append('target_lang', targetLang);
|
421 |
+
|
422 |
+
fetch('/translate/document', {
|
423 |
+
method: 'POST',
|
424 |
+
body: formData,
|
425 |
+
})
|
426 |
+
.then(response => {
|
427 |
+
if (!response.ok) {
|
428 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
429 |
}
|
430 |
+
return response.json();
|
431 |
+
})
|
432 |
+
.then(data => {
|
433 |
+
if (docLoadingIndicator) docLoadingIndicator.style.display = 'none';
|
434 |
|
435 |
+
if (data.success === false) {
|
436 |
+
throw new Error(data.error || 'Document translation failed');
|
437 |
+
}
|
438 |
|
439 |
+
// Handle language detection result if present
|
440 |
+
if (data.detected_source_lang && sourceLang === 'auto') {
|
441 |
+
showNotification(`Detected document language: ${getLanguageName(data.detected_source_lang)}`);
|
442 |
+
}
|
443 |
|
444 |
+
// Show the original text
|
445 |
+
if (docInputText) {
|
446 |
+
docInputText.textContent = data.original_text;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
447 |
|
448 |
+
// Apply RTL styling based on source language
|
449 |
+
if (data.detected_source_lang && sourceLang === 'auto') {
|
450 |
+
applyRtlStyling(data.detected_source_lang, docInputText);
|
451 |
+
} else {
|
452 |
+
applyRtlStyling(sourceLang, docInputText);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
}
|
454 |
+
}
|
455 |
+
|
456 |
+
// Show the translated text
|
457 |
+
if (docOutput) {
|
458 |
+
docOutput.textContent = data.translated_text;
|
459 |
+
|
460 |
+
// Apply RTL styling based on target language
|
461 |
+
applyRtlStyling(targetLang, docOutput);
|
462 |
+
}
|
463 |
+
|
464 |
+
// Show the document result container
|
465 |
+
if (docResult) docResult.style.display = 'block';
|
466 |
+
})
|
467 |
+
.catch(error => {
|
468 |
+
console.error('Error during document translation:', error);
|
469 |
+
|
470 |
+
if (docLoadingIndicator) docLoadingIndicator.style.display = 'none';
|
471 |
+
|
472 |
+
// Show error message
|
473 |
+
if (errorMessageElement) {
|
474 |
+
errorMessageElement.style.display = 'block';
|
475 |
+
errorMessageElement.textContent = `Document translation error: ${error.message}`;
|
476 |
+
}
|
477 |
});
|
|
|
|
|
478 |
}
|
479 |
|
480 |
+
// Helper function to get language name from code
|
481 |
+
function getLanguageName(code) {
|
482 |
+
// Hard-coded mapping for common languages
|
483 |
+
const langMap = {
|
484 |
+
'ar': 'Arabic',
|
485 |
+
'en': 'English',
|
486 |
+
'fr': 'French',
|
487 |
+
'es': 'Spanish',
|
488 |
+
'de': 'German',
|
489 |
+
'zh': 'Chinese',
|
490 |
+
'ru': 'Russian',
|
491 |
+
'ja': 'Japanese',
|
492 |
+
'hi': 'Hindi',
|
493 |
+
'auto': 'Auto-detect'
|
494 |
+
};
|
495 |
+
|
496 |
+
// Try to get from our map
|
497 |
+
if (langMap[code]) {
|
498 |
+
return langMap[code];
|
499 |
+
}
|
500 |
+
|
501 |
+
// Try to get from the select option text
|
502 |
+
if (sourceLangText) {
|
503 |
+
const option = Array.from(sourceLangText.options).find(opt => opt.value === code);
|
504 |
+
if (option) {
|
505 |
+
return option.text;
|
506 |
+
}
|
507 |
+
}
|
508 |
+
|
509 |
+
// Fallback to code
|
510 |
+
return code;
|
511 |
+
}
|
512 |
+
|
513 |
+
// Display notification
|
514 |
+
function showNotification(message) {
|
515 |
+
if (notificationElement) {
|
516 |
+
notificationElement.textContent = message;
|
517 |
+
notificationElement.style.display = 'block';
|
518 |
+
notificationElement.classList.add('show');
|
519 |
+
|
520 |
+
// Hide after 3 seconds
|
521 |
+
setTimeout(() => {
|
522 |
+
notificationElement.classList.remove('show');
|
523 |
+
setTimeout(() => {
|
524 |
+
notificationElement.style.display = 'none';
|
525 |
+
}, 300);
|
526 |
+
}, 3000);
|
527 |
}
|
|
|
528 |
}
|
529 |
+
|
530 |
+
// Initialize by applying RTL styling based on initial language selection
|
531 |
+
handleLanguageChange();
|
532 |
};
|
static/style.css
CHANGED
@@ -1,118 +1,574 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
body {
|
2 |
-
font-family: sans-serif;
|
3 |
-
margin: 20px;
|
4 |
-
background-color: #f4f4f4;
|
5 |
line-height: 1.6;
|
|
|
|
|
|
|
|
|
|
|
6 |
}
|
7 |
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
color: #333;
|
|
|
|
|
|
|
|
|
|
|
11 |
}
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
margin-bottom: 20px;
|
17 |
-
border-radius: 8px;
|
18 |
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
19 |
}
|
20 |
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
border-bottom: 1px solid #eee;
|
24 |
-
padding-bottom:
|
25 |
-
margin-
|
26 |
}
|
27 |
|
28 |
-
.
|
29 |
-
|
30 |
}
|
31 |
|
32 |
-
|
33 |
-
|
34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
font-weight: bold;
|
36 |
-
color: #333;
|
37 |
}
|
38 |
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
width: 100%;
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
|
|
|
|
|
|
47 |
}
|
48 |
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
52 |
}
|
53 |
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
color: white;
|
57 |
-
padding: 10px 15px;
|
58 |
border: none;
|
59 |
-
border-radius:
|
|
|
|
|
60 |
cursor: pointer;
|
61 |
-
|
62 |
-
|
|
|
|
|
63 |
}
|
64 |
|
65 |
-
button:hover {
|
66 |
-
background-color: #
|
67 |
}
|
68 |
|
69 |
-
.
|
70 |
-
|
71 |
-
|
72 |
-
background-color: #e9e9e9;
|
73 |
-
border: 1px solid #ddd;
|
74 |
-
border-radius: 4px;
|
75 |
-
min-height: 50px;
|
76 |
}
|
77 |
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
}
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
border-radius:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
}
|
90 |
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
font-family: monospace;
|
93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
}
|
95 |
|
96 |
-
.
|
97 |
-
margin-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
color: #a94442;
|
102 |
-
border-radius: 4px;
|
103 |
}
|
104 |
|
105 |
-
|
106 |
-
|
107 |
-
|
|
|
108 |
}
|
109 |
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
}
|
115 |
-
|
116 |
-
|
|
|
117 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
}
|
|
|
1 |
+
/* Global styles */
|
2 |
+
:root {
|
3 |
+
--primary-color: #4285F4;
|
4 |
+
--primary-dark: #3367D6;
|
5 |
+
--secondary-color: #34A853;
|
6 |
+
--accent-color: #FBBC05;
|
7 |
+
--danger-color: #EA4335;
|
8 |
+
--text-dark: #202124;
|
9 |
+
--text-light: #5f6368;
|
10 |
+
--gray-light: #f5f5f5;
|
11 |
+
--gray-medium: #e8eaed;
|
12 |
+
--gray-border: #dadce0;
|
13 |
+
--white: #ffffff;
|
14 |
+
--rtl-dir: rtl;
|
15 |
+
--ltr-dir: ltr;
|
16 |
+
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
17 |
+
--border-radius: 8px;
|
18 |
+
--transition-speed: 0.3s;
|
19 |
+
--panel-background: #f8f9fa;
|
20 |
+
}
|
21 |
+
|
22 |
+
/* Reset and base styles */
|
23 |
+
* {
|
24 |
+
margin: 0;
|
25 |
+
padding: 0;
|
26 |
+
box-sizing: border-box;
|
27 |
+
}
|
28 |
+
|
29 |
body {
|
30 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
|
|
31 |
line-height: 1.6;
|
32 |
+
color: #333;
|
33 |
+
background-color: #f9f9f9;
|
34 |
+
display: flex;
|
35 |
+
flex-direction: column;
|
36 |
+
min-height: 100vh;
|
37 |
}
|
38 |
|
39 |
+
/* Header styles */
|
40 |
+
header {
|
41 |
+
background-color: #fff;
|
42 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
43 |
+
padding: 1rem 2rem;
|
44 |
+
display: flex;
|
45 |
+
justify-content: space-between;
|
46 |
+
align-items: center;
|
47 |
+
}
|
48 |
+
|
49 |
+
.logo {
|
50 |
+
display: flex;
|
51 |
+
align-items: center;
|
52 |
+
flex-direction: column;
|
53 |
+
}
|
54 |
+
|
55 |
+
.logo h1 {
|
56 |
+
font-size: 2rem;
|
57 |
+
font-weight: 700;
|
58 |
+
}
|
59 |
+
|
60 |
+
.primary-color {
|
61 |
+
color: #4285f4;
|
62 |
+
}
|
63 |
+
|
64 |
+
.tagline {
|
65 |
+
font-size: 0.9rem;
|
66 |
+
color: #666;
|
67 |
+
margin-top: -5px;
|
68 |
+
}
|
69 |
+
|
70 |
+
nav ul {
|
71 |
+
display: flex;
|
72 |
+
list-style: none;
|
73 |
+
}
|
74 |
+
|
75 |
+
nav ul li {
|
76 |
+
margin-left: 1.5rem;
|
77 |
+
position: relative;
|
78 |
+
}
|
79 |
+
|
80 |
+
nav ul li::after {
|
81 |
+
content: '';
|
82 |
+
display: block;
|
83 |
+
width: 0;
|
84 |
+
height: 3px;
|
85 |
+
background-color: #4285f4;
|
86 |
+
position: absolute;
|
87 |
+
bottom: -10px;
|
88 |
+
left: 0;
|
89 |
+
transition: width 0.3s;
|
90 |
+
}
|
91 |
+
|
92 |
+
nav ul li.active::after,
|
93 |
+
nav ul li:hover::after {
|
94 |
+
width: 100%;
|
95 |
+
}
|
96 |
+
|
97 |
+
nav a {
|
98 |
color: #333;
|
99 |
+
text-decoration: none;
|
100 |
+
font-weight: 500;
|
101 |
+
font-size: 1rem;
|
102 |
+
padding: 0.5rem 0;
|
103 |
+
transition: color 0.3s;
|
104 |
}
|
105 |
|
106 |
+
nav a:hover,
|
107 |
+
nav ul li.active a {
|
108 |
+
color: #4285f4;
|
|
|
|
|
|
|
109 |
}
|
110 |
|
111 |
+
/* Main content styles */
|
112 |
+
main {
|
113 |
+
flex: 1;
|
114 |
+
padding: 2rem;
|
115 |
+
max-width: 1200px;
|
116 |
+
margin: 0 auto;
|
117 |
+
width: 100%;
|
118 |
+
}
|
119 |
+
|
120 |
+
.translation-section {
|
121 |
+
width: 100%;
|
122 |
+
}
|
123 |
+
|
124 |
+
.hidden {
|
125 |
+
display: none;
|
126 |
+
}
|
127 |
+
|
128 |
+
.translation-container {
|
129 |
+
background-color: #fff;
|
130 |
+
border-radius: 10px;
|
131 |
+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
132 |
+
overflow: hidden;
|
133 |
+
}
|
134 |
+
|
135 |
+
.translation-box {
|
136 |
+
padding: 1.5rem;
|
137 |
+
}
|
138 |
+
|
139 |
+
/* Language controls */
|
140 |
+
.language-controls {
|
141 |
+
display: flex;
|
142 |
+
align-items: center;
|
143 |
border-bottom: 1px solid #eee;
|
144 |
+
padding-bottom: 1.5rem;
|
145 |
+
margin-bottom: 1.5rem;
|
146 |
}
|
147 |
|
148 |
+
.language-selector {
|
149 |
+
flex: 1;
|
150 |
}
|
151 |
|
152 |
+
.lang-select {
|
153 |
+
width: 100%;
|
154 |
+
padding: 0.8rem;
|
155 |
+
border: 1px solid #ddd;
|
156 |
+
border-radius: 5px;
|
157 |
+
font-size: 1rem;
|
158 |
+
background-color: #f9f9f9;
|
159 |
+
cursor: pointer;
|
160 |
+
transition: border-color 0.3s, box-shadow 0.3s;
|
161 |
+
appearance: none;
|
162 |
+
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23777' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
163 |
+
background-repeat: no-repeat;
|
164 |
+
background-position: right 0.7rem center;
|
165 |
+
background-size: 1em;
|
166 |
+
padding-right: 2.5rem;
|
167 |
+
}
|
168 |
+
|
169 |
+
.lang-select:focus {
|
170 |
+
outline: none;
|
171 |
+
border-color: #4285f4;
|
172 |
+
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.25);
|
173 |
+
}
|
174 |
+
|
175 |
+
.lang-select option {
|
176 |
+
font-size: 0.95rem;
|
177 |
+
padding: 0.5rem;
|
178 |
+
}
|
179 |
+
|
180 |
+
.lang-select option[value="auto"] {
|
181 |
font-weight: bold;
|
|
|
182 |
}
|
183 |
|
184 |
+
.swap-languages {
|
185 |
+
margin: 0 1.5rem;
|
186 |
+
}
|
187 |
+
|
188 |
+
.swap-languages button {
|
189 |
+
background-color: #f1f1f1;
|
190 |
+
border: none;
|
191 |
+
border-radius: 50%;
|
192 |
+
width: 40px;
|
193 |
+
height: 40px;
|
194 |
+
cursor: pointer;
|
195 |
+
transition: background-color 0.3s, transform 0.3s;
|
196 |
+
display: flex;
|
197 |
+
justify-content: center;
|
198 |
+
align-items: center;
|
199 |
+
}
|
200 |
+
|
201 |
+
.swap-languages button:hover {
|
202 |
+
background-color: #e0e0e0;
|
203 |
+
transform: rotate(180deg);
|
204 |
+
}
|
205 |
+
|
206 |
+
.swap-languages i {
|
207 |
+
font-size: 1.2rem;
|
208 |
+
color: #555;
|
209 |
+
}
|
210 |
+
|
211 |
+
/* Translation panels */
|
212 |
+
.translation-panels {
|
213 |
+
display: flex;
|
214 |
+
gap: 2rem;
|
215 |
+
margin-bottom: 1.5rem;
|
216 |
+
}
|
217 |
+
|
218 |
+
.panel {
|
219 |
+
flex: 1;
|
220 |
+
border: 1px solid #eee;
|
221 |
+
border-radius: 8px;
|
222 |
+
overflow: hidden;
|
223 |
+
display: flex;
|
224 |
+
flex-direction: column;
|
225 |
+
}
|
226 |
+
|
227 |
+
.panel-header {
|
228 |
+
background-color: #f9f9f9;
|
229 |
+
padding: 0.8rem;
|
230 |
+
display: flex;
|
231 |
+
justify-content: space-between;
|
232 |
+
align-items: center;
|
233 |
+
border-bottom: 1px solid #eee;
|
234 |
+
}
|
235 |
+
|
236 |
+
.panel-title {
|
237 |
+
font-weight: 600;
|
238 |
+
color: #555;
|
239 |
+
}
|
240 |
+
|
241 |
+
.panel-actions {
|
242 |
+
display: flex;
|
243 |
+
}
|
244 |
+
|
245 |
+
.icon-button {
|
246 |
+
background: none;
|
247 |
+
border: none;
|
248 |
+
font-size: 1rem;
|
249 |
+
padding: 0.3rem;
|
250 |
+
cursor: pointer;
|
251 |
+
border-radius: 3px;
|
252 |
+
transition: background-color 0.2s;
|
253 |
+
color: #777;
|
254 |
+
}
|
255 |
+
|
256 |
+
.icon-button:hover {
|
257 |
+
background-color: #e0e0e0;
|
258 |
+
color: #4285f4;
|
259 |
+
}
|
260 |
+
|
261 |
+
textarea#text-input {
|
262 |
+
resize: none;
|
263 |
+
border: none;
|
264 |
+
padding: 1rem;
|
265 |
width: 100%;
|
266 |
+
height: 200px;
|
267 |
+
font-family: inherit;
|
268 |
+
font-size: 1rem;
|
269 |
+
}
|
270 |
+
|
271 |
+
textarea#text-input:focus {
|
272 |
+
outline: none;
|
273 |
}
|
274 |
|
275 |
+
#text-result,
|
276 |
+
.document-content {
|
277 |
+
padding: 1rem;
|
278 |
+
height: 200px;
|
279 |
+
overflow-y: auto;
|
280 |
+
background-color: #fcfcfc;
|
281 |
+
flex: 1;
|
282 |
}
|
283 |
|
284 |
+
.panel-footer {
|
285 |
+
padding: 0.8rem;
|
286 |
+
display: flex;
|
287 |
+
justify-content: space-between;
|
288 |
+
align-items: center;
|
289 |
+
background-color: #f9f9f9;
|
290 |
+
border-top: 1px solid #eee;
|
291 |
+
}
|
292 |
+
|
293 |
+
.char-count {
|
294 |
+
font-size: 0.85rem;
|
295 |
+
color: #777;
|
296 |
+
}
|
297 |
+
|
298 |
+
.translate-button {
|
299 |
+
background-color: #4285f4;
|
300 |
color: white;
|
|
|
301 |
border: none;
|
302 |
+
border-radius: 5px;
|
303 |
+
padding: 0.6rem 1.2rem;
|
304 |
+
font-size: 1rem;
|
305 |
cursor: pointer;
|
306 |
+
transition: background-color 0.3s;
|
307 |
+
display: flex;
|
308 |
+
align-items: center;
|
309 |
+
gap: 0.5rem;
|
310 |
}
|
311 |
|
312 |
+
.translate-button:hover {
|
313 |
+
background-color: #3367d6;
|
314 |
}
|
315 |
|
316 |
+
.translate-button:disabled {
|
317 |
+
background-color: #b3b3b3;
|
318 |
+
cursor: not-allowed;
|
|
|
|
|
|
|
|
|
319 |
}
|
320 |
|
321 |
+
/* Document upload area */
|
322 |
+
.file-upload-area {
|
323 |
+
border: 2px dashed #ddd;
|
324 |
+
border-radius: 10px;
|
325 |
+
padding: 2.5rem;
|
326 |
+
text-align: center;
|
327 |
+
margin: 2rem 0;
|
328 |
+
transition: border-color 0.3s, background-color 0.3s;
|
329 |
+
}
|
330 |
+
|
331 |
+
.file-upload-area:hover {
|
332 |
+
background-color: #f9f9f9;
|
333 |
+
border-color: #4285f4;
|
334 |
+
}
|
335 |
+
|
336 |
+
.file-upload-label {
|
337 |
+
display: flex;
|
338 |
+
flex-direction: column;
|
339 |
+
align-items: center;
|
340 |
+
cursor: pointer;
|
341 |
+
}
|
342 |
+
|
343 |
+
.file-upload-label i {
|
344 |
+
font-size: 2.5rem;
|
345 |
+
color: #4285f4;
|
346 |
+
margin-bottom: 1rem;
|
347 |
+
}
|
348 |
+
|
349 |
+
.file-types {
|
350 |
+
font-size: 0.85rem;
|
351 |
+
color: #777;
|
352 |
+
margin-top: 0.5rem;
|
353 |
+
}
|
354 |
+
|
355 |
+
input.file-input {
|
356 |
+
display: none;
|
357 |
+
}
|
358 |
+
|
359 |
+
#file-name-display {
|
360 |
+
margin-top: 1rem;
|
361 |
+
font-size: 0.9rem;
|
362 |
+
color: #4285f4;
|
363 |
+
font-weight: 500;
|
364 |
+
}
|
365 |
+
|
366 |
+
.document-result-area {
|
367 |
+
margin-top: 2rem;
|
368 |
+
}
|
369 |
+
|
370 |
+
.document-panels {
|
371 |
+
display: none;
|
372 |
+
}
|
373 |
+
|
374 |
+
.document-actions {
|
375 |
+
display: flex;
|
376 |
+
justify-content: center;
|
377 |
+
margin-top: 1rem;
|
378 |
+
}
|
379 |
+
|
380 |
+
.file-info {
|
381 |
+
font-size: 0.85rem;
|
382 |
+
color: #777;
|
383 |
+
}
|
384 |
+
|
385 |
+
/* Loading indicator */
|
386 |
+
.loading-indicator {
|
387 |
+
display: none;
|
388 |
+
flex-direction: column;
|
389 |
+
align-items: center;
|
390 |
+
justify-content: center;
|
391 |
+
padding: 2rem;
|
392 |
+
text-align: center;
|
393 |
+
color: #555;
|
394 |
}
|
395 |
|
396 |
+
.spinner {
|
397 |
+
width: 40px;
|
398 |
+
height: 40px;
|
399 |
+
border: 4px solid rgba(66, 133, 244, 0.2);
|
400 |
+
border-left-color: #4285f4;
|
401 |
+
border-radius: 50%;
|
402 |
+
animation: spin 1s linear infinite;
|
403 |
+
margin-bottom: 1rem;
|
404 |
+
}
|
405 |
+
|
406 |
+
@keyframes spin {
|
407 |
+
to {
|
408 |
+
transform: rotate(360deg);
|
409 |
+
}
|
410 |
}
|
411 |
|
412 |
+
/* Notification and error messages */
|
413 |
+
.notification,
|
414 |
+
.error-message {
|
415 |
+
position: fixed;
|
416 |
+
bottom: 2rem;
|
417 |
+
left: 50%;
|
418 |
+
transform: translateX(-50%);
|
419 |
+
padding: 0.8rem 1.5rem;
|
420 |
+
border-radius: 5px;
|
421 |
+
font-weight: 500;
|
422 |
+
z-index: 100;
|
423 |
+
display: none;
|
424 |
+
opacity: 0;
|
425 |
+
transition: opacity 0.3s;
|
426 |
+
max-width: 90%;
|
427 |
+
}
|
428 |
+
|
429 |
+
.notification {
|
430 |
+
background-color: #4caf50;
|
431 |
+
color: white;
|
432 |
+
}
|
433 |
+
|
434 |
+
.error-message {
|
435 |
+
background-color: #f44336;
|
436 |
+
color: white;
|
437 |
+
}
|
438 |
+
|
439 |
+
/* Footer */
|
440 |
+
footer {
|
441 |
+
background-color: #333;
|
442 |
+
color: #fff;
|
443 |
+
padding: 1.5rem;
|
444 |
+
text-align: center;
|
445 |
+
}
|
446 |
+
|
447 |
+
.footer-content p {
|
448 |
+
font-size: 0.9rem;
|
449 |
+
opacity: 0.8;
|
450 |
+
}
|
451 |
+
|
452 |
+
/* Debug info */
|
453 |
+
.debug-info {
|
454 |
+
background-color: #ffe082;
|
455 |
+
color: #333;
|
456 |
+
padding: 1rem;
|
457 |
+
margin: 1rem 0;
|
458 |
+
border-radius: 5px;
|
459 |
font-family: monospace;
|
460 |
+
display: none;
|
461 |
+
}
|
462 |
+
|
463 |
+
/* Quick phrases styles */
|
464 |
+
.quick-phrases-container {
|
465 |
+
margin-top: 1.5rem;
|
466 |
+
border-top: 1px solid #eee;
|
467 |
+
padding-top: 1.5rem;
|
468 |
+
}
|
469 |
+
|
470 |
+
.quick-phrases {
|
471 |
+
margin-bottom: 1.5rem;
|
472 |
+
background-color: var(--panel-background);
|
473 |
+
border-radius: var(--border-radius);
|
474 |
+
padding: 1rem;
|
475 |
+
border: 1px solid var(--gray-border);
|
476 |
}
|
477 |
|
478 |
+
.quick-phrases h3 {
|
479 |
+
margin-bottom: 0.8rem;
|
480 |
+
color: var(--text-dark);
|
481 |
+
font-size: 0.95rem;
|
482 |
+
font-weight: 600;
|
|
|
|
|
483 |
}
|
484 |
|
485 |
+
.phrases-container {
|
486 |
+
display: flex;
|
487 |
+
flex-wrap: wrap;
|
488 |
+
gap: 0.5rem;
|
489 |
}
|
490 |
|
491 |
+
.phrase-btn {
|
492 |
+
background-color: var(--white);
|
493 |
+
border: 1px solid var(--gray-border);
|
494 |
+
border-radius: 20px;
|
495 |
+
padding: 0.4rem 0.8rem;
|
496 |
+
font-size: 0.85rem;
|
497 |
+
cursor: pointer;
|
498 |
+
transition: all var(--transition-speed);
|
499 |
+
}
|
500 |
+
|
501 |
+
.phrase-btn:hover {
|
502 |
+
background-color: var(--primary-color);
|
503 |
+
color: var(--white);
|
504 |
+
border-color: var(--primary-color);
|
505 |
+
}
|
506 |
+
|
507 |
+
.phrase-btn.ar {
|
508 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
509 |
+
}
|
510 |
+
|
511 |
+
/* Auto-detect language badge */
|
512 |
+
.detected-language {
|
513 |
+
display: inline-block;
|
514 |
+
background-color: #e8f0fe;
|
515 |
+
color: #1a73e8;
|
516 |
+
padding: 0.2rem 0.5rem;
|
517 |
+
border-radius: 3px;
|
518 |
+
font-size: 0.8rem;
|
519 |
+
margin-left: 0.5rem;
|
520 |
+
font-weight: normal;
|
521 |
+
}
|
522 |
+
|
523 |
+
/* Responsive design */
|
524 |
+
@media (max-width: 768px) {
|
525 |
+
.translation-panels {
|
526 |
+
flex-direction: column;
|
527 |
+
}
|
528 |
+
|
529 |
+
header {
|
530 |
+
flex-direction: column;
|
531 |
+
padding: 1rem;
|
532 |
+
}
|
533 |
+
|
534 |
+
nav ul li {
|
535 |
+
margin-left: 1rem;
|
536 |
+
margin-top: 0.5rem;
|
537 |
+
}
|
538 |
+
|
539 |
+
.language-controls {
|
540 |
+
flex-direction: column;
|
541 |
+
gap: 1rem;
|
542 |
+
flex-wrap: wrap;
|
543 |
}
|
544 |
+
|
545 |
+
.language-selector {
|
546 |
+
flex-basis: 100%;
|
547 |
}
|
548 |
+
|
549 |
+
.swap-languages {
|
550 |
+
margin: 0 auto;
|
551 |
+
}
|
552 |
+
|
553 |
+
.translation-box {
|
554 |
+
padding: 1rem;
|
555 |
+
}
|
556 |
+
|
557 |
+
.file-upload-area {
|
558 |
+
padding: 1.5rem;
|
559 |
+
}
|
560 |
+
}
|
561 |
+
|
562 |
+
/* Animation for loading and transitions */
|
563 |
+
@keyframes fadeIn {
|
564 |
+
from {
|
565 |
+
opacity: 0;
|
566 |
+
}
|
567 |
+
to {
|
568 |
+
opacity: 1;
|
569 |
+
}
|
570 |
+
}
|
571 |
+
|
572 |
+
.translated-text {
|
573 |
+
animation: fadeIn 0.4s ease-in-out;
|
574 |
}
|
templates/index.html
CHANGED
@@ -1,133 +1,255 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
-
<html lang="en"
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>
|
7 |
<link rel="stylesheet" href="/static/style.css">
|
8 |
-
<
|
9 |
-
/* Add some styling for better visibility of debug and error messages */
|
10 |
-
#error-message {
|
11 |
-
background-color: #ffebee;
|
12 |
-
color: #c62828;
|
13 |
-
padding: 10px;
|
14 |
-
margin: 10px 0;
|
15 |
-
border-radius: 4px;
|
16 |
-
border: 1px solid #ef9a9a;
|
17 |
-
display: none;
|
18 |
-
}
|
19 |
-
#debug-info {
|
20 |
-
background-color: #e3f2fd;
|
21 |
-
color: #1565c0;
|
22 |
-
padding: 10px;
|
23 |
-
margin: 10px 0;
|
24 |
-
border-radius: 4px;
|
25 |
-
border: 1px solid #90caf9;
|
26 |
-
font-family: monospace;
|
27 |
-
white-space: pre-wrap;
|
28 |
-
display: none;
|
29 |
-
}
|
30 |
-
.loading-spinner {
|
31 |
-
margin: 10px 0;
|
32 |
-
padding: 10px;
|
33 |
-
background-color: #e8f5e9;
|
34 |
-
border-radius: 4px;
|
35 |
-
border: 1px solid #a5d6a7;
|
36 |
-
display: none;
|
37 |
-
}
|
38 |
-
.result-box {
|
39 |
-
margin-top: 20px;
|
40 |
-
padding: 15px;
|
41 |
-
background-color: #f5f5f5;
|
42 |
-
border-radius: 4px;
|
43 |
-
display: none;
|
44 |
-
}
|
45 |
-
</style>
|
46 |
</head>
|
47 |
<body>
|
48 |
-
<
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
<form id="text-translation-form">
|
53 |
-
<div class="input-group">
|
54 |
-
<label for="source-lang-text">Source Language:</label>
|
55 |
-
<select id="source-lang-text" name="source_lang">
|
56 |
-
<option value="en">English</option>
|
57 |
-
<option value="fr">French</option>
|
58 |
-
<option value="es">Spanish</option>
|
59 |
-
<option value="de">German</option>
|
60 |
-
<option value="ar">Arabic</option> <!- Added Arabic as source ->
|
61 |
-
<option value="auto">Auto-Detect (Not implemented)</option>
|
62 |
-
<!- Add more languages as needed ->
|
63 |
-
</select>
|
64 |
-
</div>
|
65 |
-
<div class="input-group">
|
66 |
-
<label for="target-lang-text">Target Language:</label>
|
67 |
-
<select id="target-lang-text" name="target_lang">
|
68 |
-
<option value="ar">Arabic (MSA Fusha)</option>
|
69 |
-
<!- Add other target languages if reverse translation is implemented ->
|
70 |
-
<!- <option value="en">English</option> ->
|
71 |
-
</select>
|
72 |
-
</div>
|
73 |
-
<textarea id="text-input" name="text" placeholder="Enter text to translate..."></textarea>
|
74 |
-
<div id="text-loading" class="loading-spinner">Translating...</div>
|
75 |
-
<button type="submit">Translate Text</button>
|
76 |
-
</form>
|
77 |
-
<div id="text-result" class="result-box" dir="rtl"> <!- Set default to RTL for Arabic output ->
|
78 |
-
<h3>Translation:</h3>
|
79 |
-
<pre><code id="text-output"></code></pre>
|
80 |
</div>
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
<input type="file" id="doc-input" name="file" accept=".pdf,.docx,.xlsx,.pptx,.txt">
|
113 |
-
</div>
|
114 |
-
<div class="form-group">
|
115 |
-
<button type="submit" class="btn btn-primary">Translate Document</button>
|
116 |
</div>
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
</div>
|
121 |
-
<p>Translating document... This may take a few minutes for large files or first-time use.</p>
|
122 |
</div>
|
123 |
-
</
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
|
|
|
|
|
|
|
|
129 |
</div>
|
130 |
-
</
|
131 |
|
132 |
<script src="/static/script.js"></script>
|
133 |
</body>
|
|
|
1 |
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Tarjama | Smart Translation Service</title>
|
7 |
<link rel="stylesheet" href="/static/style.css">
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
</head>
|
10 |
<body>
|
11 |
+
<header>
|
12 |
+
<div class="logo">
|
13 |
+
<h1><span class="primary-color">Tarjama</span></h1>
|
14 |
+
<p class="tagline">Smart Translation</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
</div>
|
16 |
+
<nav>
|
17 |
+
<ul>
|
18 |
+
<li class="active"><a href="#text-translation">Text</a></li>
|
19 |
+
<li><a href="#document-translation">Documents</a></li>
|
20 |
+
</ul>
|
21 |
+
</nav>
|
22 |
+
</header>
|
23 |
|
24 |
+
<main>
|
25 |
+
<section id="text-translation" class="translation-section">
|
26 |
+
<div class="translation-container">
|
27 |
+
<div class="translation-box">
|
28 |
+
<div class="language-controls">
|
29 |
+
<div class="language-selector">
|
30 |
+
<select id="source-lang-text" name="source_lang" class="lang-select">
|
31 |
+
<option value="auto">Detect Language</option>
|
32 |
+
<option value="en">English</option>
|
33 |
+
<option value="fr">French</option>
|
34 |
+
<option value="es">Spanish</option>
|
35 |
+
<option value="de">German</option>
|
36 |
+
<option value="ar">Arabic</option>
|
37 |
+
<option value="zh">Chinese</option>
|
38 |
+
<option value="ru">Russian</option>
|
39 |
+
<option value="ja">Japanese</option>
|
40 |
+
<option value="hi">Hindi</option>
|
41 |
+
<option value="pt">Portuguese</option>
|
42 |
+
<option value="tr">Turkish</option>
|
43 |
+
<option value="it">Italian</option>
|
44 |
+
</select>
|
45 |
+
</div>
|
46 |
+
|
47 |
+
<div class="swap-languages">
|
48 |
+
<button type="button" id="swap-languages" aria-label="Swap languages">
|
49 |
+
<i class="fas fa-exchange-alt"></i>
|
50 |
+
</button>
|
51 |
+
</div>
|
52 |
+
|
53 |
+
<div class="language-selector">
|
54 |
+
<select id="target-lang-text" name="target_lang" class="lang-select">
|
55 |
+
<option value="ar">Arabic</option>
|
56 |
+
<option value="en">English</option>
|
57 |
+
<option value="fr">French</option>
|
58 |
+
<option value="es">Spanish</option>
|
59 |
+
<option value="de">German</option>
|
60 |
+
<option value="zh">Chinese</option>
|
61 |
+
<option value="ru">Russian</option>
|
62 |
+
<option value="ja">Japanese</option>
|
63 |
+
<option value="hi">Hindi</option>
|
64 |
+
<option value="pt">Portuguese</option>
|
65 |
+
<option value="tr">Turkish</option>
|
66 |
+
<option value="it">Italian</option>
|
67 |
+
</select>
|
68 |
+
</div>
|
69 |
+
</div>
|
70 |
+
|
71 |
+
<div class="translation-panels">
|
72 |
+
<div class="panel source-panel">
|
73 |
+
<div class="panel-header">
|
74 |
+
<span class="panel-title">Original Text</span>
|
75 |
+
<div class="panel-actions">
|
76 |
+
<button type="button" id="clear-source" class="icon-button" aria-label="Clear text">
|
77 |
+
<i class="fas fa-times"></i>
|
78 |
+
</button>
|
79 |
+
</div>
|
80 |
+
</div>
|
81 |
+
<form id="text-translation-form">
|
82 |
+
<textarea id="text-input" name="text" placeholder="Enter text to translate..." autofocus></textarea>
|
83 |
+
<div class="panel-footer">
|
84 |
+
<div class="char-count"><span id="char-count">0</span> characters</div>
|
85 |
+
<button type="submit" class="translate-button">
|
86 |
+
<i class="fas fa-language"></i> Translate
|
87 |
+
</button>
|
88 |
+
</div>
|
89 |
+
</form>
|
90 |
+
</div>
|
91 |
+
|
92 |
+
<div class="panel target-panel">
|
93 |
+
<div class="panel-header">
|
94 |
+
<span class="panel-title">Translation</span>
|
95 |
+
<div class="panel-actions">
|
96 |
+
<button type="button" id="copy-translation" class="icon-button" aria-label="Copy translation">
|
97 |
+
<i class="fas fa-copy"></i>
|
98 |
+
</button>
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
<div id="text-result">
|
102 |
+
<pre><code id="text-output"></code></pre>
|
103 |
+
</div>
|
104 |
+
<div id="text-loading" class="loading-indicator">
|
105 |
+
<div class="spinner"></div>
|
106 |
+
<span>Translating...</span>
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
</div>
|
110 |
|
111 |
+
<!-- Quick phrases section -->
|
112 |
+
<div class="quick-phrases-container">
|
113 |
+
<h3>Quick Phrases</h3>
|
114 |
+
<div class="quick-phrases">
|
115 |
+
<button type="button" class="phrase-btn" data-phrase="Hello, how are you?" data-auto-translate="true">Hello</button>
|
116 |
+
<button type="button" class="phrase-btn" data-phrase="Thank you very much" data-auto-translate="true">Thank you</button>
|
117 |
+
<button type="button" class="phrase-btn" data-phrase="Where is the nearest hospital?" data-auto-translate="true">Emergency</button>
|
118 |
+
<button type="button" class="phrase-btn" data-phrase="I need help, please" data-auto-translate="true">Help</button>
|
119 |
+
<button type="button" class="phrase-btn" data-phrase="How much does this cost?" data-auto-translate="true">Price</button>
|
120 |
+
<button type="button" class="phrase-btn" data-phrase="I don't understand" data-auto-translate="true">Confused</button>
|
121 |
+
</div>
|
122 |
+
</div>
|
123 |
+
|
124 |
+
<!-- Quick Translation Phrases Section -->
|
125 |
+
<div class="quick-phrases">
|
126 |
+
<h3>Frequently Used Phrases</h3>
|
127 |
+
<div class="phrase-buttons">
|
128 |
+
<button type="button" class="phrase-btn" data-text="Hello, how are you?">Hello, how are you?</button>
|
129 |
+
<button type="button" class="phrase-btn" data-text="Thank you very much">Thank you very much</button>
|
130 |
+
<button type="button" class="phrase-btn" data-text="Excuse me, where is...?">Excuse me, where is...?</button>
|
131 |
+
<button type="button" class="phrase-btn" data-text="I don't understand">I don't understand</button>
|
132 |
+
<button type="button" class="phrase-btn" data-text="How much does it cost?">How much does it cost?</button>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
</div>
|
|
|
|
|
|
|
|
|
136 |
</div>
|
137 |
+
</section>
|
138 |
+
|
139 |
+
<section id="document-translation" class="translation-section hidden">
|
140 |
+
<div class="translation-container">
|
141 |
+
<div class="translation-box">
|
142 |
+
<form id="doc-translation-form" enctype="multipart/form-data">
|
143 |
+
<div class="language-controls">
|
144 |
+
<div class="language-selector">
|
145 |
+
<select id="source-lang-doc" name="source_lang" class="lang-select">
|
146 |
+
<option value="auto">Detect Language</option>
|
147 |
+
<option value="en">English</option>
|
148 |
+
<option value="fr">French</option>
|
149 |
+
<option value="es">Spanish</option>
|
150 |
+
<option value="de">German</option>
|
151 |
+
<option value="ar">Arabic</option>
|
152 |
+
<option value="zh">Chinese</option>
|
153 |
+
<option value="ru">Russian</option>
|
154 |
+
<option value="ja">Japanese</option>
|
155 |
+
<option value="hi">Hindi</option>
|
156 |
+
<option value="pt">Portuguese</option>
|
157 |
+
<option value="tr">Turkish</option>
|
158 |
+
<option value="it">Italian</option>
|
159 |
+
</select>
|
160 |
+
</div>
|
161 |
+
|
162 |
+
<div class="swap-languages">
|
163 |
+
<button type="button" id="swap-languages-doc" aria-label="Swap languages">
|
164 |
+
<i class="fas fa-exchange-alt"></i>
|
165 |
+
</button>
|
166 |
+
</div>
|
167 |
+
|
168 |
+
<div class="language-selector">
|
169 |
+
<select id="target-lang-doc" name="target_lang" class="lang-select">
|
170 |
+
<option value="ar">Arabic</option>
|
171 |
+
<option value="en">English</option>
|
172 |
+
<option value="fr">French</option>
|
173 |
+
<option value="es">Spanish</option>
|
174 |
+
<option value="de">German</option>
|
175 |
+
<option value="zh">Chinese</option>
|
176 |
+
<option value="ru">Russian</option>
|
177 |
+
<option value="ja">Japanese</option>
|
178 |
+
<option value="hi">Hindi</option>
|
179 |
+
<option value="pt">Portuguese</option>
|
180 |
+
<option value="tr">Turkish</option>
|
181 |
+
<option value="it">Italian</option>
|
182 |
+
</select>
|
183 |
+
</div>
|
184 |
+
</div>
|
185 |
+
|
186 |
+
<div class="file-upload-area">
|
187 |
+
<label for="doc-input" class="file-upload-label">
|
188 |
+
<i class="fas fa-cloud-upload-alt"></i>
|
189 |
+
<span>Choose a file or drag it here</span>
|
190 |
+
<span class="file-types">(.pdf, .docx, .txt)</span>
|
191 |
+
</label>
|
192 |
+
<input type="file" id="doc-input" name="file" accept=".pdf,.docx,.txt" class="file-input">
|
193 |
+
<div id="file-name-display"></div>
|
194 |
+
</div>
|
195 |
+
|
196 |
+
<div class="document-actions">
|
197 |
+
<button type="submit" class="translate-button">
|
198 |
+
<i class="fas fa-language"></i> Translate Document
|
199 |
+
</button>
|
200 |
+
</div>
|
201 |
+
</form>
|
202 |
+
|
203 |
+
<div id="doc-loading" class="loading-indicator">
|
204 |
+
<div class="spinner"></div>
|
205 |
+
<span>Translating document...</span>
|
206 |
+
<p>This may take a few moments depending on file size.</p>
|
207 |
+
</div>
|
208 |
+
|
209 |
+
<div class="document-result-area">
|
210 |
+
<div id="doc-result" class="document-panels">
|
211 |
+
<div class="panel source-panel">
|
212 |
+
<div class="panel-header">
|
213 |
+
<span class="panel-title">Original Document</span>
|
214 |
+
<div class="file-info">
|
215 |
+
<span id="doc-filename"></span>
|
216 |
+
(<span id="doc-source-lang"></span>)
|
217 |
+
</div>
|
218 |
+
</div>
|
219 |
+
<div class="document-content">
|
220 |
+
<pre><code id="doc-input-text"></code></pre>
|
221 |
+
</div>
|
222 |
+
</div>
|
223 |
+
|
224 |
+
<div class="panel target-panel">
|
225 |
+
<div class="panel-header">
|
226 |
+
<span class="panel-title">Translated Document</span>
|
227 |
+
<div class="panel-actions">
|
228 |
+
<button type="button" id="copy-doc-translation" class="icon-button">
|
229 |
+
<i class="fas fa-copy"></i>
|
230 |
+
</button>
|
231 |
+
</div>
|
232 |
+
</div>
|
233 |
+
<div class="document-content">
|
234 |
+
<pre><code id="doc-output"></code></pre>
|
235 |
+
</div>
|
236 |
+
</div>
|
237 |
+
</div>
|
238 |
+
</div>
|
239 |
</div>
|
|
|
240 |
</div>
|
241 |
+
</section>
|
242 |
+
</main>
|
243 |
+
|
244 |
+
<div id="notification" class="notification"></div>
|
245 |
+
<div id="error-message" class="error-message"></div>
|
246 |
+
<div id="debug-info" class="debug-info"></div>
|
247 |
+
|
248 |
+
<footer>
|
249 |
+
<div class="footer-content">
|
250 |
+
<p>© 2025 Tarjama - Smart Translation Service</p>
|
251 |
</div>
|
252 |
+
</footer>
|
253 |
|
254 |
<script src="/static/script.js"></script>
|
255 |
</body>
|