Mohamed Atef commited on
Commit
e45ef70
·
1 Parent(s): f8feb00

fix 3 bugs : Bug 1 — No "apply to page" button in the correct tab

Browse files

Bug 2 — Apply replaces the whole field instead of the selected text
Bug 3 — Quran translation (14 languages)

extension/content-inline.js CHANGED
@@ -56,6 +56,7 @@
56
 
57
  let activeField = null;
58
  let lastInteractedField = null; // persists after focus moves to side panel (Change 1)
 
59
  let analysisSuppressed = false; // true after a non-correction model write-back (Change 3)
60
  let lastAnalyzedText = '';
61
  let suggestions = [];
@@ -111,6 +112,8 @@
111
  // event at all — neither should count as a manual edit (Change 3).
112
  const isUserKeystroke = !!e && e.isTrusted === true;
113
  if (isUserKeystroke && analysisSuppressed) analysisSuppressed = false;
 
 
114
 
115
  const text = getFieldText(activeField);
116
 
@@ -563,6 +566,9 @@
563
  }
564
  if (suggestions.length > 0) {
565
  try {
 
 
 
566
  // Tag the source field so write-back can re-find it after focus
567
  // moves to the side panel (Change 1).
568
  if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
@@ -663,16 +669,81 @@
663
 
664
  const NON_CORRECTION_SOURCES = ['summarize', 'dialect', 'quran'];
665
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  function writeTextToField(field, text, mode, source) {
667
  if (!field || typeof text !== 'string') return;
668
 
669
  const tag = field.tagName.toLowerCase();
670
  const suppress = NON_CORRECTION_SOURCES.includes(source);
671
 
 
 
 
 
 
 
672
  if (tag === 'textarea' || tag === 'input') {
673
- if (mode === 'replaceSelection'
 
 
 
 
 
 
674
  && typeof field.selectionStart === 'number'
675
  && field.selectionStart !== field.selectionEnd) {
 
676
  const start = field.selectionStart;
677
  const end = field.selectionEnd;
678
  field.value = field.value.slice(0, start) + text + field.value.slice(end);
@@ -689,14 +760,31 @@
689
  } else {
690
  // contenteditable — mirror the overlay-safe approach used by applyFix.
691
  field.focus();
692
- const sel = window.getSelection();
693
- const hasLiveSelection = sel && sel.rangeCount > 0 && field.contains(sel.anchorNode);
694
- // NOTE: execCommand('insertText') fires a TRUSTED input event, which
695
- // onFieldInput would treat as a user keystroke and clear suppression.
696
- // So only take that path when NOT suppressing; suppressed writes use the
697
- // textContent path, whose dispatched event is untrusted (Change 3-safe).
698
- if (mode === 'replaceSelection' && hasLiveSelection && !sel.isCollapsed && !suppress) {
699
- document.execCommand('insertText', false, text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  } else {
701
  field.textContent = text;
702
  if (suppress) analysisSuppressed = true;
@@ -704,7 +792,8 @@
704
  }
705
  }
706
 
707
- // Clear the source tag now that the write succeeded.
 
708
  try { delete field.dataset.bayanSource; } catch {}
709
  }
710
 
 
56
 
57
  let activeField = null;
58
  let lastInteractedField = null; // persists after focus moves to side panel (Change 1)
59
+ let pendingSelection = null; // selection captured on right-click, for selection-only write-back
60
  let analysisSuppressed = false; // true after a non-correction model write-back (Change 3)
61
  let lastAnalyzedText = '';
62
  let suggestions = [];
 
112
  // event at all — neither should count as a manual edit (Change 3).
113
  const isUserKeystroke = !!e && e.isTrusted === true;
114
  if (isUserKeystroke && analysisSuppressed) analysisSuppressed = false;
115
+ // A real edit invalidates any selection captured at right-click time.
116
+ if (isUserKeystroke) pendingSelection = null;
117
 
118
  const text = getFieldText(activeField);
119
 
 
566
  }
567
  if (suggestions.length > 0) {
568
  try {
569
+ // FAB = whole-field intent. Drop any selection captured by an earlier
570
+ // right-click so write-back replaces the entire field, not a range.
571
+ pendingSelection = null;
572
  // Tag the source field so write-back can re-find it after focus
573
  // moves to the side panel (Change 1).
574
  if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
 
669
 
670
  const NON_CORRECTION_SOURCES = ['summarize', 'dialect', 'quran'];
671
 
672
+ // Capture the field + selection range at right-click time. The context menu
673
+ // flow always acts on the selected text, so we remember exactly what was
674
+ // selected and splice the result into THAT range on write-back (Bug 2).
675
+ function captureSelection(target) {
676
+ const winSel = window.getSelection();
677
+
678
+ // Prefer the already-tracked editable field (it's what write-back resolves
679
+ // via lastInteractedField); fall back to the right-clicked target. For
680
+ // contenteditable this avoids latching onto a child node of the root.
681
+ let field = null;
682
+ const tracked = lastInteractedField
683
+ || (isEditableField(document.activeElement) ? document.activeElement : null);
684
+ if (tracked) {
685
+ const ttag = tracked.tagName.toLowerCase();
686
+ if (ttag === 'textarea' || ttag === 'input') field = tracked;
687
+ else if (tracked.isContentEditable && winSel && winSel.rangeCount > 0
688
+ && tracked.contains(winSel.anchorNode)) field = tracked;
689
+ }
690
+ if (!field && isEditableField(target)) field = target;
691
+ if (!field) { pendingSelection = null; return; }
692
+
693
+ const tag = field.tagName.toLowerCase();
694
+ if (tag === 'textarea' || tag === 'input') {
695
+ const start = field.selectionStart;
696
+ const end = field.selectionEnd;
697
+ if (typeof start === 'number' && typeof end === 'number' && start !== end) {
698
+ pendingSelection = { field, type: 'input', start, end };
699
+ } else {
700
+ pendingSelection = null;
701
+ }
702
+ } else if (field.isContentEditable) {
703
+ if (winSel && winSel.rangeCount > 0 && !winSel.isCollapsed && field.contains(winSel.anchorNode)) {
704
+ pendingSelection = { field, type: 'ce', range: winSel.getRangeAt(0).cloneRange() };
705
+ } else {
706
+ pendingSelection = null;
707
+ }
708
+ } else {
709
+ pendingSelection = null;
710
+ }
711
+
712
+ // Persist a link to the field so write-back can re-find it after focus
713
+ // moves to the side panel, even when this was a plain right-click.
714
+ if (pendingSelection) {
715
+ lastInteractedField = field;
716
+ try { field.dataset.bayanSource = '1'; } catch {}
717
+ }
718
+ }
719
+
720
+ document.addEventListener('contextmenu', (e) => {
721
+ try { captureSelection(e.target); } catch { pendingSelection = null; }
722
+ }, true);
723
+
724
  function writeTextToField(field, text, mode, source) {
725
  if (!field || typeof text !== 'string') return;
726
 
727
  const tag = field.tagName.toLowerCase();
728
  const suppress = NON_CORRECTION_SOURCES.includes(source);
729
 
730
+ // Resolve the effective mode. 'auto' = replace the captured selection if we
731
+ // have one for THIS field, otherwise replace the whole field.
732
+ const sel = (pendingSelection && pendingSelection.field === field) ? pendingSelection : null;
733
+ let effectiveMode = mode;
734
+ if (mode === 'auto') effectiveMode = sel ? 'replaceSelection' : 'replaceAll';
735
+
736
  if (tag === 'textarea' || tag === 'input') {
737
+ if (effectiveMode === 'replaceSelection' && sel && sel.type === 'input') {
738
+ const before = field.value.slice(0, sel.start);
739
+ const after = field.value.slice(sel.end);
740
+ field.value = before + text + after;
741
+ const caret = sel.start + text.length;
742
+ try { field.setSelectionRange(caret, caret); } catch {}
743
+ } else if (effectiveMode === 'replaceSelection'
744
  && typeof field.selectionStart === 'number'
745
  && field.selectionStart !== field.selectionEnd) {
746
+ // Live selection still present (no captured range) — splice into it.
747
  const start = field.selectionStart;
748
  const end = field.selectionEnd;
749
  field.value = field.value.slice(0, start) + text + field.value.slice(end);
 
760
  } else {
761
  // contenteditable — mirror the overlay-safe approach used by applyFix.
762
  field.focus();
763
+ if (effectiveMode === 'replaceSelection' && sel && sel.type === 'ce') {
764
+ // Restore the captured range, then replace its contents with the text.
765
+ try {
766
+ const winSel = window.getSelection();
767
+ winSel.removeAllRanges();
768
+ winSel.addRange(sel.range);
769
+ sel.range.deleteContents();
770
+ sel.range.insertNode(document.createTextNode(text));
771
+ winSel.collapseToEnd();
772
+ } catch {
773
+ field.textContent = text;
774
+ }
775
+ if (suppress) analysisSuppressed = true;
776
+ field.dispatchEvent(new Event('input', { bubbles: true }));
777
+ } else if (effectiveMode === 'replaceSelection') {
778
+ const winSel = window.getSelection();
779
+ const hasLive = winSel && winSel.rangeCount > 0 && !winSel.isCollapsed && field.contains(winSel.anchorNode);
780
+ if (hasLive) {
781
+ winSel.getRangeAt(0).deleteContents();
782
+ winSel.getRangeAt(0).insertNode(document.createTextNode(text));
783
+ } else {
784
+ field.textContent = text;
785
+ }
786
+ if (suppress) analysisSuppressed = true;
787
+ field.dispatchEvent(new Event('input', { bubbles: true }));
788
  } else {
789
  field.textContent = text;
790
  if (suppress) analysisSuppressed = true;
 
792
  }
793
  }
794
 
795
+ // Consume the captured selection + source tag now that the write succeeded.
796
+ pendingSelection = null;
797
  try { delete field.dataset.bayanSource; } catch {}
798
  }
799
 
extension/sidepanel/sidepanel.css CHANGED
@@ -378,6 +378,49 @@ body {
378
  color: var(--sp-text-muted);
379
  }
380
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  /* ══════════════════════════════════════════════
382
  Highlighted text (from bayan-renderer.js)
383
  ══════════════════════════════════════════════ */
 
378
  color: var(--sp-text-muted);
379
  }
380
 
381
+ /* ══════════════════════════════════════════════
382
+ Quran verse translation
383
+ ══════════════════════════════════════════════ */
384
+ .sp-quran-translate {
385
+ padding: 10px 12px 12px;
386
+ border-top: 1px solid var(--sp-border);
387
+ }
388
+
389
+ .sp-quran-translate-row {
390
+ display: flex;
391
+ align-items: center;
392
+ gap: 8px;
393
+ flex-wrap: wrap;
394
+ }
395
+
396
+ .sp-quran-translate-label {
397
+ font-size: 12px;
398
+ font-weight: 600;
399
+ color: var(--sp-text-secondary);
400
+ }
401
+
402
+ .sp-select {
403
+ flex: 1;
404
+ min-width: 140px;
405
+ padding: 6px 10px;
406
+ font-size: 13px;
407
+ color: var(--sp-text);
408
+ background: var(--sp-surface);
409
+ border: 1px solid var(--sp-border);
410
+ border-radius: 8px;
411
+ cursor: pointer;
412
+ }
413
+
414
+ .sp-select:focus {
415
+ outline: none;
416
+ border-color: var(--sp-primary);
417
+ }
418
+
419
+ .sp-select option {
420
+ background: var(--sp-surface);
421
+ color: var(--sp-text);
422
+ }
423
+
424
  /* ══════════════════════════════════════════════
425
  Highlighted text (from bayan-renderer.js)
426
  ══════════════════════════════════════════════ */
extension/sidepanel/sidepanel.html CHANGED
@@ -112,6 +112,9 @@
112
  <div class="sp-result-header">
113
  <span>النص المصحح</span>
114
  <div class="sp-result-actions">
 
 
 
115
  <button class="sp-btn-icon" id="btn-apply-all" title="تطبيق الكل">
116
  <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
117
  </button>
@@ -246,6 +249,39 @@
246
  </div>
247
  <div class="sp-result-text" id="quran-text" dir="rtl"></div>
248
  <div class="sp-summary-meta" id="quran-meta"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  </div>
250
  </section>
251
 
 
112
  <div class="sp-result-header">
113
  <span>النص المصحح</span>
114
  <div class="sp-result-actions">
115
+ <button class="sp-btn-icon" id="btn-apply-page" title="تطبيق في الصفحة">
116
+ <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
117
+ </button>
118
  <button class="sp-btn-icon" id="btn-apply-all" title="تطبيق الكل">
119
  <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
120
  </button>
 
249
  </div>
250
  <div class="sp-result-text" id="quran-text" dir="rtl"></div>
251
  <div class="sp-summary-meta" id="quran-meta"></div>
252
+
253
+ <!-- Verse translation (14 languages) -->
254
+ <div class="sp-quran-translate is-hidden" id="quran-translate-section">
255
+ <div class="sp-quran-translate-row">
256
+ <span class="sp-quran-translate-label">ترجمة الآية</span>
257
+ <select id="quran-lang-select" class="sp-select" dir="ltr">
258
+ <option value="">— اختر لغة —</option>
259
+ <option value="english">🇬🇧 English</option>
260
+ <option value="french">🇫🇷 Français</option>
261
+ <option value="turkish">🇹🇷 Türkçe</option>
262
+ <option value="persian">🇮🇷 فارسی</option>
263
+ <option value="russian">🇷🇺 Русский</option>
264
+ <option value="spanish">🇪🇸 Español</option>
265
+ <option value="german">🇩🇪 Deutsch</option>
266
+ <option value="indonesian">🇮🇩 Indonesia</option>
267
+ <option value="malay">🇲🇾 Melayu</option>
268
+ <option value="bengali">🇧🇩 বাংলা</option>
269
+ <option value="bosnian">🇧🇦 Bosanski</option>
270
+ <option value="portuguese">🇵🇹 Português</option>
271
+ <option value="uzbek">🇺🇿 O'zbek</option>
272
+ </select>
273
+ </div>
274
+ <div class="sp-result is-hidden" id="quran-translation-result" style="margin-top:8px;">
275
+ <div class="sp-result-header">
276
+ <span>الترجمة</span>
277
+ <button class="sp-btn-icon" id="btn-copy-quran-translation" title="نسخ الترجمة">
278
+ <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
279
+ </button>
280
+ </div>
281
+ <div class="sp-result-text" id="quran-translation-text" dir="auto"></div>
282
+ <div class="sp-summary-meta" id="quran-translation-ref"></div>
283
+ </div>
284
+ </div>
285
  </div>
286
  </section>
287
 
extension/sidepanel/sidepanel.js CHANGED
@@ -27,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
27
  const btnCorrect = document.getElementById('btn-correct');
28
  const btnClear = document.getElementById('btn-clear');
29
  const btnApplyAll = document.getElementById('btn-apply-all');
 
30
  const btnCopyResult = document.getElementById('btn-copy-result');
31
  const scoreSection = document.getElementById('score-section');
32
  const resultSection = document.getElementById('result-section');
@@ -155,7 +156,7 @@ document.addEventListener('DOMContentLoaded', () => {
155
  // directly; it relays through background.js. `source` lets the content
156
  // script decide whether to re-analyze (correct) or suppress (Change 3).
157
  // ══════════════════════════════════════════════════════════
158
- function writeBackToPage(text, mode = 'replaceAll', source = 'correct') {
159
  try {
160
  chrome.runtime.sendMessage(
161
  { type: 'WRITE_BACK_TO_PAGE', text, mode, source },
@@ -234,7 +235,6 @@ document.addEventListener('DOMContentLoaded', () => {
234
  renderSuggestions(currentSuggestions);
235
  resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
236
  saveState();
237
- writeBackToPage(analyzedText, 'replaceAll', 'correct');
238
  showToast('✓ تم التصحيح');
239
  });
240
  });
@@ -379,10 +379,24 @@ document.addEventListener('DOMContentLoaded', () => {
379
  updateScore(0, 0, 0);
380
  renderSuggestions([]);
381
  saveState();
382
- writeBackToPage(analyzedText, 'replaceAll', 'correct');
383
  showToast('✓ تم تطبيق جميع التصحيحات');
384
  });
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  btnCopyResult.addEventListener('click', () => {
387
  const text = resultText.textContent || '';
388
  navigator.clipboard.writeText(text)
@@ -478,6 +492,22 @@ document.addEventListener('DOMContentLoaded', () => {
478
  const quranText = document.getElementById('quran-text');
479
  const quranMeta = document.getElementById('quran-meta');
480
  const btnCopyQuran = document.getElementById('btn-copy-quran');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
  if (quranInput) {
483
  quranInput.addEventListener('input', () => updateCounts(quranInput, quranCharCount, null));
@@ -490,6 +520,10 @@ document.addEventListener('DOMContentLoaded', () => {
490
  try {
491
  const data = await bayanQuran(text);
492
  quranResultSection.classList.remove('is-hidden');
 
 
 
 
493
  if (data.error) {
494
  quranText.textContent = data.error;
495
  quranMeta.textContent = '';
@@ -498,6 +532,9 @@ document.addEventListener('DOMContentLoaded', () => {
498
  quranMeta.textContent = data.matched_segment && data.full_verse
499
  ? `المقطع المطابق: ${data.matched_segment}`
500
  : '';
 
 
 
501
  showToast('✓ تم التدقيق');
502
  }
503
  } catch (error) {
@@ -508,6 +545,42 @@ document.addEventListener('DOMContentLoaded', () => {
508
  }
509
  });
510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  btnCopyQuran.addEventListener('click', () => {
512
  navigator.clipboard.writeText(quranText.textContent || '')
513
  .then(() => showToast('✓ تم النسخ'))
@@ -622,7 +695,7 @@ document.addEventListener('DOMContentLoaded', () => {
622
  btn.addEventListener('click', () => {
623
  const text = (getText() || '').trim();
624
  if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
625
- writeBackToPage(text, 'replaceAll', source);
626
  });
627
  anchorBtn.parentElement.appendChild(btn);
628
  }
 
27
  const btnCorrect = document.getElementById('btn-correct');
28
  const btnClear = document.getElementById('btn-clear');
29
  const btnApplyAll = document.getElementById('btn-apply-all');
30
+ const btnApplyPage = document.getElementById('btn-apply-page');
31
  const btnCopyResult = document.getElementById('btn-copy-result');
32
  const scoreSection = document.getElementById('score-section');
33
  const resultSection = document.getElementById('result-section');
 
156
  // directly; it relays through background.js. `source` lets the content
157
  // script decide whether to re-analyze (correct) or suppress (Change 3).
158
  // ══════════════════════════════════════════════════════════
159
+ function writeBackToPage(text, mode = 'auto', source = 'correct') {
160
  try {
161
  chrome.runtime.sendMessage(
162
  { type: 'WRITE_BACK_TO_PAGE', text, mode, source },
 
235
  renderSuggestions(currentSuggestions);
236
  resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
237
  saveState();
 
238
  showToast('✓ تم التصحيح');
239
  });
240
  });
 
379
  updateScore(0, 0, 0);
380
  renderSuggestions([]);
381
  saveState();
382
+ writeBackToPage(analyzedText, 'auto', 'correct');
383
  showToast('✓ تم تطبيق جميع التصحيحات');
384
  });
385
 
386
+ // Explicit "apply corrected text to the page field" button (Bug 1).
387
+ // Writes the current corrected text — with any still-pending suggestions
388
+ // applied — back into the source field, honouring selection vs whole-field.
389
+ if (btnApplyPage) {
390
+ btnApplyPage.addEventListener('click', () => {
391
+ if (!analyzedText) { showToast('لا يوجد نص للتطبيق'); return; }
392
+ if (isStale) { showToast('⚠ أعد التحليل أولاً — النص تغيّر'); return; }
393
+ const finalText = currentSuggestions.length > 0
394
+ ? applyAllPatches(analyzedText, currentSuggestions)
395
+ : analyzedText;
396
+ writeBackToPage(finalText, 'auto', 'correct');
397
+ });
398
+ }
399
+
400
  btnCopyResult.addEventListener('click', () => {
401
  const text = resultText.textContent || '';
402
  navigator.clipboard.writeText(text)
 
492
  const quranText = document.getElementById('quran-text');
493
  const quranMeta = document.getElementById('quran-meta');
494
  const btnCopyQuran = document.getElementById('btn-copy-quran');
495
+ // Translation sub-UI
496
+ const quranTranslateSection = document.getElementById('quran-translate-section');
497
+ const quranLangSelect = document.getElementById('quran-lang-select');
498
+ const quranTranslationResult = document.getElementById('quran-translation-result');
499
+ const quranTranslationText = document.getElementById('quran-translation-text');
500
+ const quranTranslationRef = document.getElementById('quran-translation-ref');
501
+ const btnCopyQuranTranslation = document.getElementById('btn-copy-quran-translation');
502
+ let lastQuranQuery = '';
503
+
504
+ // Parse the API's "(verse text) 【surah:ayah】" segment into {text, ref}.
505
+ function parseQuranSegment(seg) {
506
+ seg = seg || '';
507
+ const refMatch = seg.match(/【([^】]+)】/);
508
+ const text = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
509
+ return { text, ref: refMatch ? refMatch[1] : '' };
510
+ }
511
 
512
  if (quranInput) {
513
  quranInput.addEventListener('input', () => updateCounts(quranInput, quranCharCount, null));
 
520
  try {
521
  const data = await bayanQuran(text);
522
  quranResultSection.classList.remove('is-hidden');
523
+ // Reset translation UI for the new query.
524
+ if (quranTranslateSection) quranTranslateSection.classList.add('is-hidden');
525
+ if (quranTranslationResult) quranTranslationResult.classList.add('is-hidden');
526
+ if (quranLangSelect) quranLangSelect.value = '';
527
  if (data.error) {
528
  quranText.textContent = data.error;
529
  quranMeta.textContent = '';
 
532
  quranMeta.textContent = data.matched_segment && data.full_verse
533
  ? `المقطع المطابق: ${data.matched_segment}`
534
  : '';
535
+ // Enable translation now that we have a verified verse.
536
+ lastQuranQuery = text;
537
+ if (quranTranslateSection) quranTranslateSection.classList.remove('is-hidden');
538
  showToast('✓ تم التدقيق');
539
  }
540
  } catch (error) {
 
545
  }
546
  });
547
 
548
+ // Translate the verified verse into the chosen language.
549
+ if (quranLangSelect) {
550
+ quranLangSelect.addEventListener('change', async () => {
551
+ const lang = quranLangSelect.value;
552
+ if (!lang || !lastQuranQuery) return;
553
+
554
+ setLoading(true, 'جارٍ الترجمة...');
555
+ try {
556
+ const data = await bayanQuran(lastQuranQuery, lang);
557
+ quranTranslationResult.classList.remove('is-hidden');
558
+ if (data.error) {
559
+ quranTranslationText.textContent = data.error;
560
+ quranTranslationRef.textContent = '';
561
+ } else {
562
+ const parsed = parseQuranSegment(data.matched_segment || data.full_verse || '');
563
+ quranTranslationText.textContent = parsed.text || data.full_verse || '';
564
+ quranTranslationRef.textContent = parsed.ref ? `[${parsed.ref}]` : '';
565
+ showToast('✓ تمت الترجمة');
566
+ }
567
+ } catch (error) {
568
+ console.error('[Bayan SP] Quran translation error:', error);
569
+ showToast('خطأ في الاتصال — تحقق من الإنترنت');
570
+ } finally {
571
+ setLoading(false);
572
+ }
573
+ });
574
+ }
575
+
576
+ if (btnCopyQuranTranslation) {
577
+ btnCopyQuranTranslation.addEventListener('click', () => {
578
+ navigator.clipboard.writeText(quranTranslationText.textContent || '')
579
+ .then(() => showToast('✓ تم نسخ الترجمة'))
580
+ .catch(() => showToast('تعذّر النسخ'));
581
+ });
582
+ }
583
+
584
  btnCopyQuran.addEventListener('click', () => {
585
  navigator.clipboard.writeText(quranText.textContent || '')
586
  .then(() => showToast('✓ تم النسخ'))
 
695
  btn.addEventListener('click', () => {
696
  const text = (getText() || '').trim();
697
  if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
698
+ writeBackToPage(text, 'auto', source);
699
  });
700
  anchorBtn.parentElement.appendChild(btn);
701
  }