amine_dubs commited on
Commit
ed71793
·
1 Parent(s): f01bab8
Files changed (4) hide show
  1. backend/main.py +343 -105
  2. static/script.js +498 -159
  3. static/style.css +529 -73
  4. 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 and tokenizer variables ---
63
- translator = None
64
- tokenizer = None
65
- model = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 the translation model and tokenizer."""
74
- global translator, tokenizer, model, model_initialization_attempts, last_initialization_attempt
 
 
 
 
 
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
- print(f"Initializing model and tokenizer (attempt {model_initialization_attempts})...")
 
89
 
90
- # Use a smaller, faster model
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
- test_result = translator("hello world", max_length=128)
152
- print(f"Model test result: {test_result}")
 
 
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
- global translator, tokenizer, model
 
174
 
175
  print(f"Translation Request - Source Lang: {source_lang}, Target Lang: {target_lang}")
176
 
177
- # Check if model is initialized, if not try to initialize it
178
- if not model or not tokenizer or not translator:
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
- try:
185
- # Ensure only the raw text is sent to the Helsinki model
186
- text_to_translate = text
187
- print(f"Translating text (first 50 chars): {text_to_translate[:50]}...") # Log the actual text being sent
188
 
189
- # Use a more reliable timeout approach with concurrent.futures
190
- with concurrent.futures.ThreadPoolExecutor() as executor:
191
- future = executor.submit(
192
- lambda: translator(
193
- text_to_translate, # Pass only the raw text
194
- max_length=768
195
- )[0]["translation_text"]
196
- )
 
 
 
 
 
197
 
198
- try:
199
- # Set a reasonable timeout
200
- result = future.result(timeout=10)
201
-
202
- # Post-process the result for Arabic cultural adaptation if needed
203
- if target_lang == "ar":
204
- result = culturally_adapt_arabic(result)
 
205
 
206
- print(f"Translation successful (first 50 chars): {result[:50]}...")
207
- return result
208
- except concurrent.futures.TimeoutError:
209
- print(f"Model inference timed out after 10 seconds, falling back to online translation")
210
- return use_fallback_translation(text, source_lang, target_lang)
211
- except Exception as e:
212
- print(f"Error during model inference: {e}")
213
- # If the model failed during inference, try to re-initialize it for next time
214
- # but use fallback for this request
215
- initialize_model()
216
- return use_fallback_translation(text, source_lang, target_lang)
217
- except Exception as e:
218
- print(f"Error using local model: {e}")
219
- traceback.print_exc()
 
 
 
 
 
 
 
 
 
 
 
 
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 translator, model, tokenizer
 
 
 
 
 
 
 
244
 
245
  try:
246
  # If model isn't initialized yet, try to initialize it
247
- if not model or not tokenizer or not translator:
248
- print("Model not initialized. Attempting initialization...")
249
- return initialize_model()
250
 
251
  # Test the existing model with a simple translation
252
- test_text = "hello"
 
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" # Additional server
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 (8 seconds instead of 5)
306
- response = requests.post(server, json=payload, headers=headers, timeout=8)
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
- return {"success": True, "translated_text": translation_result}
 
 
 
 
 
 
 
 
 
 
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 (Updated Code Check)") # Added log
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
- if not extracted_text:
451
- print("[DEBUG] No text extracted from document.")
452
- raise HTTPException(status_code=400, detail="Could not extract any text from the document.")
 
 
 
 
 
 
 
 
 
 
 
453
 
454
- # Translate the extracted text using the updated translate_text function
455
- print("[DEBUG] Calling translate_text for document content...")
456
  translated_text = translate_text(extracted_text, source_lang, target_lang)
457
-
458
- print(f"[DEBUG] Document translation successful. Returning result for {file.filename}")
459
- return JSONResponse(content={
 
460
  "original_filename": file.filename,
461
- "detected_source_lang": source_lang, # Assuming source_lang is detected or provided correctly
462
  "translated_text": translated_text
463
- })
464
-
465
- except HTTPException as http_exc:
466
- # Re-raise HTTPExceptions directly
467
- raise http_exc
 
 
 
 
 
 
468
  except Exception as e:
469
- print(f"Document translation error: {e}")
470
  traceback.print_exc()
471
- # Return a generic error response
472
- raise HTTPException(status_code=500, detail=f"Document translation error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 translation app');
4
-
5
- // Get form elements ONCE after load
6
- const textTranslationForm = document.querySelector('#text-translation-form');
7
- const docTranslationForm = document.querySelector('#doc-translation-form');
8
-
9
- // Get text form OUTPUT elements ONCE
10
- const textLoadingElement = document.getElementById('text-loading');
11
- const debugElement = document.getElementById('debug-info');
12
- const errorElement = document.getElementById('error-message');
13
- const textResultBox = document.getElementById('text-result');
14
- const textOutputElement = document.getElementById('text-output');
15
-
16
- // Check if essential text elements were found on load (excluding inputs used via FormData)
17
- if (!textTranslationForm) { // Only check form itself now
18
- console.error('CRITICAL: Text translation form not found on window load!');
19
- if (errorElement) {
20
- errorElement.textContent = 'Error: Could not find the text translation form element. Check HTML ID.';
21
- errorElement.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
- return;
24
  }
25
 
26
- // Set up text translation form
27
- console.log('Text translation form found on load');
28
- textTranslationForm.addEventListener('submit', function(event) {
29
- event.preventDefault();
30
- console.log('Text translation form submitted');
 
31
 
32
- // Hide previous results/errors
33
- if (textResultBox) textResultBox.style.display = 'none';
34
- if (errorElement) errorElement.style.display = 'none';
35
- if (debugElement) debugElement.style.display = 'none';
36
 
37
- // --- Use FormData Approach ---
38
- try {
39
- const formData = new FormData(event.target); // event.target is the form
40
-
41
- // Get values using the 'name' attributes from the HTML
42
- const text = formData.get('text') ? formData.get('text').trim() : '';
43
- const sourceLangValue = formData.get('source_lang');
44
- const targetLangValue = formData.get('target_lang');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- console.log('[DEBUG] FormData values - Text:', text, 'Source:', sourceLangValue, 'Target:', targetLangValue);
47
-
48
  if (!text) {
49
- showError('Please enter text to translate');
50
  return;
51
  }
52
 
53
- if (!sourceLangValue || !targetLangValue) {
54
- console.error('Could not read language values from FormData!');
55
- showError('Internal error: Language selection missing.');
 
 
 
 
 
 
 
 
 
 
 
 
56
  return;
57
  }
58
 
59
- // Show loading indicator
60
- if (textLoadingElement) textLoadingElement.style.display = 'block';
 
61
 
62
- // Create payload
63
- const payload = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  text: text,
65
- source_lang: sourceLangValue,
66
- target_lang: targetLangValue
67
- };
68
-
69
- console.log('Sending payload:', payload);
70
-
71
- // Send API request
72
- fetch('/translate/text', {
73
- method: 'POST',
74
- headers: { 'Content-Type': 'application/json' },
75
- body: JSON.stringify(payload)
76
- })
77
- .then(response => {
78
- if (!response.ok) throw new Error('Server error: ' + response.status);
79
- return response.text(); // Get raw text first
80
- })
81
- .then(responseText => {
82
- console.log('Response received:', responseText);
83
- let data;
84
- try {
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
- if (!data.success || !data.translated_text) {
92
- throw new Error(data.error || 'No translation returned or success flag false');
 
 
 
 
 
 
 
 
 
 
 
93
  }
 
 
 
 
 
94
 
95
- // Show result
96
- if (textResultBox && textOutputElement) {
97
- textOutputElement.textContent = data.translated_text;
98
- textResultBox.style.display = 'block';
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
- // Get form elements
129
- const fileInput = docTranslationForm.querySelector('#doc-input');
130
- const loadingIndicator = document.querySelector('#doc-loading');
 
 
131
 
132
- if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
133
- showError('Please select a document to upload');
134
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
 
 
 
 
136
 
137
- // Show loading indicator
138
- if (loadingIndicator) loadingIndicator.style.display = 'block';
 
139
 
140
- // Create form data
141
- const formData = new FormData(docTranslationForm);
 
 
142
 
143
- // Make API request
144
- fetch('/translate/document', {
145
- method: 'POST',
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
- // Display result
160
- const resultBox = document.querySelector('#doc-result');
161
- const outputEl = document.querySelector('#doc-output');
162
- const filenameEl = document.querySelector('#doc-filename');
163
- const sourceLangEl = document.querySelector('#doc-source-lang');
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 show errors
186
- function showError(message) {
187
- if (errorElement) {
188
- errorElement.textContent = 'Error: ' + message;
189
- errorElement.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- h1 {
9
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  color: #333;
 
 
 
 
 
11
  }
12
 
13
- .container {
14
- background: #fff;
15
- padding: 20px;
16
- margin-bottom: 20px;
17
- border-radius: 8px;
18
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
19
  }
20
 
21
- h2 {
22
- color: #555;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  border-bottom: 1px solid #eee;
24
- padding-bottom: 10px;
25
- margin-top: 0;
26
  }
27
 
28
- .input-group {
29
- margin-bottom: 15px;
30
  }
31
 
32
- label {
33
- display: block;
34
- margin-bottom: 5px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  font-weight: bold;
36
- color: #333;
37
  }
38
 
39
- select,
40
- textarea,
41
- input[type="file"] {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  width: 100%;
43
- padding: 10px;
44
- border: 1px solid #ccc;
45
- border-radius: 4px;
46
- box-sizing: border-box; /* Prevents padding from adding to width */
 
 
 
47
  }
48
 
49
- textarea {
50
- min-height: 100px;
51
- resize: vertical;
 
 
 
 
52
  }
53
 
54
- button {
55
- background-color: #5cb85c;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  color: white;
57
- padding: 10px 15px;
58
  border: none;
59
- border-radius: 4px;
 
 
60
  cursor: pointer;
61
- font-size: 1em;
62
- transition: background-color 0.3s ease;
 
 
63
  }
64
 
65
- button:hover {
66
- background-color: #4cae4c;
67
  }
68
 
69
- .result-box {
70
- margin-top: 20px;
71
- padding: 15px;
72
- background-color: #e9e9e9;
73
- border: 1px solid #ddd;
74
- border-radius: 4px;
75
- min-height: 50px;
76
  }
77
 
78
- .result-box h3 {
79
- margin-top: 0;
80
- color: #333;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
 
83
- pre {
84
- white-space: pre-wrap; /* Allows text to wrap */
85
- word-wrap: break-word; /* Breaks long words */
86
- background: #f9f9f9;
87
- padding: 10px;
88
- border-radius: 4px;
 
 
 
 
 
 
 
 
89
  }
90
 
91
- code {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  font-family: monospace;
93
- font-size: 0.95em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
 
96
- .error-box {
97
- margin-top: 20px;
98
- padding: 15px;
99
- background-color: #f2dede;
100
- border: 1px solid #ebccd1;
101
- color: #a94442;
102
- border-radius: 4px;
103
  }
104
 
105
- /* RTL support for results */
106
- [dir="rtl"] {
107
- text-align: right;
 
108
  }
109
 
110
- /* Basic responsiveness */
111
- @media (max-width: 600px) {
112
- body {
113
- margin: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
- .container {
116
- padding: 15px;
 
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" dir="ltr"> <!- Default to LTR, can be changed dynamically ->
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AI Translator</title>
7
  <link rel="stylesheet" href="/static/style.css">
8
- <style>
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
- <h1>AI Translation Service</h1>
49
-
50
- <div class="container">
51
- <h2>Direct Text Translation</h2>
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
- </div>
 
 
 
 
 
 
82
 
83
- <!-- Add debug info container -->
84
- <div id="debug-info"></div>
85
- <div id="error-message" class="error-box"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- <div class="container">
88
- <h2>Document Translation</h2>
89
- <form id="doc-translation-form" enctype="multipart/form-data">
90
- <div class="input-group">
91
- <label for="source-lang-doc">Source Language:</label>
92
- <select id="source-lang-doc" name="source_lang">
93
- <option value="en">English</option>
94
- <option value="fr">French</option>
95
- <option value="es">Spanish</option>
96
- <option value="de">German</option>
97
- <option value="ar">Arabic</option> <!- Added Arabic as source ->
98
- <option value="auto">Auto-Detect (Not implemented)</option>
99
- <!- Add more languages as needed ->
100
- </select>
101
- </div>
102
- <div class="input-group">
103
- <label for="target-lang-doc">Target Language:</label>
104
- <select id="target-lang-doc" name="target_lang">
105
- <option value="ar">Arabic (MSA Fusha)</option>
106
- <!- Add other target languages if reverse translation is implemented ->
107
- <!- <option value="en">English</option> ->
108
- </select>
109
- </div>
110
- <div class="input-group">
111
- <label for="doc-input">Upload Document (.pdf, .docx, .xlsx, .pptx, .txt):</label>
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
- <div id="doc-loading" class="loading-spinner" style="display: none;">
118
- <div class="spinner-border text-primary" role="status">
119
- <span class="sr-only">Loading...</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </div>
121
- <p>Translating document... This may take a few minutes for large files or first-time use.</p>
122
  </div>
123
- </form>
124
- <div id="doc-result" class="result-box" dir="rtl"> <!- Set default to RTL for Arabic output ->
125
- <h3>Translation:</h3>
126
- <p>Original Filename: <span id="doc-filename"></span></p>
127
- <p>Detected Source Language: <span id="doc-source-lang"></span></p>
128
- <pre><code id="doc-output"></code></pre>
 
 
 
 
129
  </div>
130
- </div>
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>&copy; 2025 Tarjama - Smart Translation Service</p>
251
  </div>
252
+ </footer>
253
 
254
  <script src="/static/script.js"></script>
255
  </body>