PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.84
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.84
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / admin / js / ai-page-translator.js

ai-page-translator.js in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.84, at includes/admin/js/ai-page-translator.js

3,623 lines 150.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function($, elementor) {
2 'use strict';
3
4 // Check if Elementor and our AI settings exist
5 if (!elementor || !window.KingAddonsAiField) {
6 return;
7 }
8
9 // Translation progress tracking
10 var translationState = {
11 isTranslating: false,
12 totalElements: 0,
13 translatedElements: 0,
14 failedElements: 0,
15 currentElement: null,
16 fromLang: '',
17 toLang: '',
18 isCancelled: false,
19 currentRequests: [], // Store active AJAX requests to cancel them
20 doneElementIds: [], // Elements finished in this run, for resuming later
21 failedElementIds: [],
22 consecutiveFailures: 0,
23 resumedCount: 0,
24 lastErrorMessage: ''
25 };
26
27 // Saved progress lets a run continue after the editor is reloaded.
28 var PROGRESS_STORAGE_PREFIX = 'king_addons_ai_translator_progress_';
29 var PROGRESS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // A week-old run is stale.
30
31 /**
32 * Id of the document currently open in the editor, or 0 when unknown.
33 */
34 function getCurrentDocumentId() {
35 try {
36 var doc = elementor.documents.getCurrent();
37 return doc && doc.id ? parseInt(doc.id, 10) : 0;
38 } catch (e) {
39 return 0;
40 }
41 }
42
43 function getProgressStorageKey(documentId) {
44 return PROGRESS_STORAGE_PREFIX + (documentId || getCurrentDocumentId());
45 }
46
47 /**
48 * Persist where the run got to. Storage can be unavailable (private mode,
49 * blocked site data), and losing resume support must never break a run.
50 */
51 function saveTranslationProgress() {
52 var documentId = getCurrentDocumentId();
53 if (!documentId || !translationState.totalElements) {
54 return;
55 }
56
57 try {
58 window.localStorage.setItem(getProgressStorageKey(documentId), JSON.stringify({
59 v: 1,
60 documentId: documentId,
61 fromLang: translationState.fromLang,
62 toLang: translationState.toLang,
63 total: translationState.totalElements,
64 done: translationState.doneElementIds,
65 failed: translationState.failedElementIds,
66 updatedAt: Date.now()
67 }));
68 } catch (e) {
69 // Ignore - resuming is a convenience, not a requirement.
70 }
71 }
72
73 function clearTranslationProgress() {
74 try {
75 window.localStorage.removeItem(getProgressStorageKey());
76 } catch (e) {
77 // Ignore.
78 }
79 }
80
81 /**
82 * Saved progress for the open document, or null when there is nothing
83 * usable to resume.
84 */
85 function loadTranslationProgress() {
86 var documentId = getCurrentDocumentId();
87 if (!documentId) {
88 return null;
89 }
90
91 var raw;
92 try {
93 raw = window.localStorage.getItem(getProgressStorageKey(documentId));
94 } catch (e) {
95 return null;
96 }
97
98 if (!raw) {
99 return null;
100 }
101
102 var saved;
103 try {
104 saved = JSON.parse(raw);
105 } catch (e) {
106 clearTranslationProgress();
107 return null;
108 }
109
110 var valid = saved
111 && saved.v === 1
112 && saved.documentId === documentId
113 && saved.toLang
114 && Array.isArray(saved.done)
115 && typeof saved.total === 'number';
116
117 if (!valid) {
118 clearTranslationProgress();
119 return null;
120 }
121
122 // Drop stale entries, and finished ones that were never cleaned up.
123 if ((Date.now() - (saved.updatedAt || 0)) > PROGRESS_MAX_AGE_MS || saved.done.length >= saved.total) {
124 clearTranslationProgress();
125 return null;
126 }
127
128 return saved;
129 }
130
131 // Language options
132 var languages = {
133 'en': 'English',
134 'es': 'Spanish (Español)',
135 'fr': 'French (Français)',
136 'de': 'German (Deutsch)',
137 'it': 'Italian (Italiano)',
138 'pt': 'Portuguese (Português)',
139 'ru': 'Russian (Русский)',
140 'ja': 'Japanese (日本語)',
141 'ko': 'Korean (한국어)',
142 'zh': 'Chinese (中文)',
143 'ar': 'Arabic (العربية)',
144 'hi': 'Hindi (हिन्दी)',
145 'nl': 'Dutch (Nederlands)',
146 'pl': 'Polish (Polski)',
147 'tr': 'Turkish (Türkçe)',
148 'uk': 'Ukrainian (Українська)',
149 'cs': 'Czech (Čeština)',
150 'sv': 'Swedish (Svenska)',
151 'no': 'Norwegian (Norsk)',
152 'da': 'Danish (Dansk)',
153 'fi': 'Finnish (Suomi)'
154 };
155
156 /**
157 * Check if premium version is active
158 */
159 function isPremiumActive() {
160 // Check for premium indicators
161 return !!(
162 window.KingAddonsPro ||
163 window.kingAddonsPro ||
164 (window.KingAddonsAiField && window.KingAddonsAiField.is_pro) ||
165 (window.KingAddonsAiField && window.KingAddonsAiField.premium_active) ||
166 document.querySelector('body.king-addons-pro') ||
167 (typeof jQuery !== 'undefined' && jQuery('body').hasClass('king-addons-pro'))
168 );
169 }
170
171 /**
172 * Inject CSS styles for the translator
173 */
174 function injectTranslatorStyles() {
175 if ($('#king-addons-ai-translator-styles').length === 0) {
176 const styles = `
177 <style id="king-addons-ai-translator-styles">
178 /* Design tokens - flat surfaces, one accent, no gradients. */
179 :root {
180 --ka-tr-accent: #5B03FF;
181 --ka-tr-accent-hover: #4A02D6;
182 --ka-tr-accent-soft: rgba(91, 3, 255, 0.08);
183 --ka-tr-ink: #16161a;
184 --ka-tr-ink-muted: #6b7280;
185 --ka-tr-surface: #ffffff;
186 --ka-tr-surface-sunken: #f6f7f9;
187 --ka-tr-border: #e4e6ea;
188 --ka-tr-border-strong: #d3d6db;
189 --ka-tr-success: #10794a;
190 --ka-tr-success-soft: #eefaf3;
191 --ka-tr-success-border: #c2e9d4;
192 --ka-tr-warning: #8a5a00;
193 --ka-tr-warning-soft: #fff8ec;
194 --ka-tr-warning-border: #f3ddb4;
195 --ka-tr-danger: #b3261e;
196 --ka-tr-radius: 12px;
197 --ka-tr-radius-sm: 8px;
198 --ka-tr-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
199 }
200
201 /* Translator Button Styles */
202 /* Desaturated violet at the toolbar's own 4px radius: the
203 saturated fill shimmered against the near-black bar and
204 its 8px corners did not match any neighbouring control. */
205 .king-addons-ai-translator-btn {
206 background: #6C5CE7 !important;
207 border: none !important;
208 color: #fff !important;
209 padding: 8px 14px !important;
210 border-radius: 4px !important;
211 font-size: 12px !important;
212 font-weight: 600 !important;
213 cursor: pointer !important;
214 display: inline-flex !important;
215 align-items: center !important;
216 gap: 6px !important;
217 transition: background-color 0.15s ease !important;
218 margin: 8px !important;
219 position: relative !important;
220 z-index: 10 !important;
221 text-decoration: none !important;
222 outline: none !important;
223 box-shadow: none !important;
224 }
225 .king-addons-ai-translator-btn:hover {
226 background: #5B4BD6 !important;
227 box-shadow: none !important;
228 }
229 .king-addons-ai-translator-btn:focus-visible {
230 outline: 2px solid #8C7DFF !important;
231 outline-offset: 2px !important;
232 }
233 .king-addons-ai-translator-btn img {
234 width: 16px !important;
235 height: 16px !important;
236 flex-shrink: 0 !important;
237 }
238 .king-addons-ai-translator-btn span {
239 white-space: nowrap !important;
240 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
241 }
242
243 /* Location-specific styles */
244
245 /* In panel header */
246 .king-addons-translator-location-panel-header {
247 position: absolute !important;
248 top: 50% !important;
249 right: 16px !important;
250 transform: translateY(-50%) !important;
251 margin: 0 !important;
252 z-index: 1000 !important;
253 }
254 .king-addons-translator-location-panel-header:hover {
255 transform: translateY(-50%) translateY(-1px) !important;
256 }
257
258 /* In header within panel */
259 .king-addons-translator-location-header-in-panel {
260 margin-left: auto !important;
261 margin-right: 8px !important;
262 }
263
264 /* At top of panel */
265 .king-addons-translator-location-panel-top {
266 width: calc(100% - 16px) !important;
267 margin: 8px !important;
268 justify-content: center !important;
269 }
270
271 /* In general elementor panel */
272 .king-addons-translator-location-elementor-panel {
273 margin: 8px !important;
274 align-self: flex-end !important;
275 }
276
277 /* Toolbar button group integration styles */
278 .king-addons-translator-location-left-group,
279 .king-addons-translator-location-toolbar-stack,
280 .king-addons-translator-location-toolbar,
281 .king-addons-translator-location-grid-stack {
282 /* Material UI button styling is handled in the HTML structure */
283 display: inline-flex !important;
284 }
285
286 /* Additional spacing for toolbar button */
287 .king-addons-translator-location-left-group .king-addons-ai-translator-btn,
288 .king-addons-translator-location-toolbar-stack .king-addons-ai-translator-btn,
289 .king-addons-translator-location-grid-stack .king-addons-ai-translator-btn {
290 margin-left: 8px !important;
291 }
292
293 /* Compact popup styles */
294 .king-addons-translator-popup.compact .king-addons-translator-progress-text {
295 font-size: 14px;
296 margin-bottom: 8px;
297 }
298
299 .king-addons-translator-popup.compact .king-addons-translator-current-element {
300 font-size: 12px;
301 margin-top: 8px;
302 color: #666;
303 overflow: hidden;
304 text-overflow: ellipsis;
305 white-space: nowrap;
306 }
307
308 .king-addons-translator-popup.compact .king-addons-translator-stats {
309 margin: 12px 0;
310 }
311
312 .king-addons-translator-popup.compact .king-addons-translator-stat {
313 margin: 0 8px;
314 }
315
316 .king-addons-translator-popup.compact .king-addons-translator-stat-number {
317 font-size: 18px;
318 }
319
320 .king-addons-translator-popup.compact .king-addons-translator-stat-label {
321 font-size: 11px;
322 }
323
324 /* Compact mode adjustments for custom fields */
325 .king-addons-translator-popup.compact .king-addons-prompt-examples {
326 padding: 6px;
327 margin-top: 4px;
328 }
329
330 .king-addons-translator-popup.compact .king-addons-prompt-examples small {
331 font-size: 10px;
332 }
333
334 .king-addons-translator-popup.compact .king-addons-pro-info {
335 font-size: 11px;
336 margin-top: 8px;
337 padding: 6px 8px;
338 background: #f8f9fa;
339 border-radius: 4px;
340 border-left: 3px solid #5B03FF;
341 }
342
343 /* Element highlighting styles moved to preview iframe */
344
345 /* Ensure button appears properly in all panel locations */
346 #elementor-panel .king-addons-ai-translator-btn,
347 .elementor-panel .king-addons-ai-translator-btn {
348 max-width: 200px !important;
349 overflow: hidden !important;
350 }
351
352 /* Responsive behavior */
353 @media (max-width: 600px) {
354 /* Hide text in panel buttons on small screens */
355 .king-addons-ai-translator-btn span {
356 display: none !important;
357 }
358 .king-addons-ai-translator-btn {
359 padding: 8px !important;
360 min-width: 32px !important;
361 }
362 }
363
364 /* Popup Overlay */
365 .king-addons-translator-overlay {
366 position: fixed;
367 top: 0;
368 left: 0;
369 right: 0;
370 bottom: 0;
371 background: rgba(16, 16, 20, 0.55);
372 z-index: 999999;
373 display: flex;
374 align-items: center;
375 justify-content: center;
376 transition: opacity 0.3s ease;
377 }
378
379 .king-addons-translator-overlay.hiding {
380 opacity: 0;
381 pointer-events: none;
382 }
383
384 /* Popup Container */
385 .king-addons-translator-popup {
386 --ka-tr-pad: 28px;
387 background: var(--ka-tr-surface);
388 padding: var(--ka-tr-pad);
389 border-radius: var(--ka-tr-radius);
390 box-shadow: 0 1px 2px rgba(16,16,20,0.06), 0 12px 32px rgba(16,16,20,0.16);
391 width: 90%;
392 max-width: 480px;
393 max-height: 82vh;
394 overflow-y: auto;
395 transition: all 0.3s ease;
396 transform: scale(1);
397 font-family: var(--ka-tr-font);
398 color: var(--ka-tr-ink);
399 line-height: 1.5;
400 }
401
402 /* Compact popup for top-right positioning */
403 .king-addons-translator-popup.compact {
404 --ka-tr-pad: 16px;
405 position: fixed;
406 top: 80px;
407 right: 20px;
408 width: 350px;
409 max-width: 350px;
410 padding: var(--ka-tr-pad);
411 z-index: 999999;
412 max-height: 400px;
413 transform: scale(1);
414 box-shadow: 0 8px 32px rgba(0,0,0,0.4);
415 }
416
417 /* Compact popup header */
418 .king-addons-translator-popup.compact h3 {
419 font-size: 16px;
420 margin: 0 0 12px 0;
421 display: flex;
422 justify-content: space-between;
423 align-items: center;
424 }
425
426 /* Close button for compact popup */
427 .king-addons-translator-close-btn {
428 background: none;
429 border: none;
430 font-size: 18px;
431 cursor: pointer;
432 color: #999;
433 width: 24px;
434 height: 24px;
435 display: flex;
436 align-items: center;
437 justify-content: center;
438 border-radius: 3px;
439 }
440
441 .king-addons-translator-close-btn:hover {
442 background: #f0f0f0;
443 color: #333;
444 }
445
446 /* Animation states */
447 .king-addons-translator-popup.moving {
448 transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
449 }
450
451 /* Notification banner animation */
452 @keyframes slideDown {
453 0% {
454 transform: translateY(-100%);
455 opacity: 0;
456 }
457 100% {
458 transform: translateY(0);
459 opacity: 1;
460 }
461 }
462
463 /* Pulse animation for success numbers */
464 @keyframes pulse {
465 0% {
466 transform: scale(1);
467 opacity: 1;
468 }
469 50% {
470 transform: scale(1.1);
471 opacity: 0.8;
472 }
473 100% {
474 transform: scale(1);
475 opacity: 1;
476 }
477 }
478
479 .king-addons-translator-popup h3 {
480 margin: 0 0 6px 0;
481 font-size: 18px;
482 font-weight: 650;
483 letter-spacing: -0.01em;
484 color: var(--ka-tr-ink);
485 display: flex;
486 align-items: center;
487 gap: 10px;
488 }
489
490 .king-addons-translator-form {
491 display: flex;
492 flex-direction: column;
493 gap: 16px;
494 }
495
496 .king-addons-translator-field {
497 display: flex;
498 flex-direction: column;
499 gap: 6px;
500 }
501
502 .king-addons-translator-field label {
503 font-weight: 600;
504 color: var(--ka-tr-ink);
505 font-size: 13px;
506 }
507
508 .king-addons-translator-field select {
509 padding: 10px 12px;
510 border: 1px solid var(--ka-tr-border-strong);
511 border-radius: var(--ka-tr-radius-sm);
512 font-size: 14px;
513 height: auto;
514 background: var(--ka-tr-surface);
515 color: var(--ka-tr-ink);
516 }
517
518 .king-addons-translator-field select:focus {
519 border-color: #5B03FF;
520 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
521 outline: none;
522 }
523
524 .king-addons-translator-field input[type="text"] {
525 padding: 10px 12px;
526 border: 1px solid var(--ka-tr-border-strong);
527 border-radius: var(--ka-tr-radius-sm);
528 font-size: 14px;
529 margin-top: 6px;
530 transition: border-color 0.3s ease, box-shadow 0.3s ease;
531 }
532
533 .king-addons-translator-field input[type="text"]:focus {
534 border-color: #5B03FF;
535 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
536 outline: none;
537 }
538
539 .king-addons-custom-language-field {
540 margin-top: 8px;
541 display: none;
542 animation: slideDown 0.3s ease-out;
543 }
544
545 .king-addons-custom-language-field.show {
546 display: block;
547 }
548
549 .king-addons-custom-language-field input {
550 width: 100%;
551 box-sizing: border-box;
552 }
553
554 .king-addons-custom-language-field label {
555 font-size: 13px;
556 color: #666;
557 margin-bottom: 4px;
558 display: block;
559 }
560
561 .king-addons-pro-badge {
562 background: #f5b301;
563 color: #3a2c00;
564 font-size: 10px;
565 font-weight: bold;
566 padding: 2px 6px;
567 border-radius: 3px;
568 margin-left: 6px;
569 vertical-align: middle;
570 }
571
572 /* Style for disabled custom option when not premium */
573 .king-addons-translator-field select option[value="custom"]:disabled {
574 color: #999;
575 background-color: #f5f5f5;
576 }
577
578 /* Enhanced styling for custom language fields */
579 .king-addons-custom-language-field.show input:focus {
580 border-color: #5B03FF;
581 box-shadow: 0 0 0 2px rgba(91,3,255,0.1);
582 }
583
584 /* Info text for premium features */
585 .king-addons-pro-info {
586 font-size: 12px;
587 color: var(--ka-tr-ink-muted);
588 margin-top: 4px;
589 line-height: 1.5;
590 background: var(--ka-tr-surface-sunken);
591 border: 1px solid var(--ka-tr-border);
592 border-radius: var(--ka-tr-radius-sm);
593 padding: 12px 14px;
594 }
595
596 .king-addons-pro-info a {
597 color: #5B03FF;
598 text-decoration: none;
599 font-weight: 500;
600 }
601
602 .king-addons-pro-info a:hover {
603 color: #4f00e6;
604 text-decoration: underline;
605 }
606
607 /* Prompt examples styling */
608 .king-addons-prompt-examples {
609 margin-top: 6px;
610 padding: 10px 12px;
611 background: var(--ka-tr-surface-sunken);
612 border: 1px solid var(--ka-tr-border);
613 border-radius: var(--ka-tr-radius-sm);
614 }
615
616 .king-addons-prompt-examples small {
617 color: #666;
618 font-size: 11px;
619 line-height: 1.4;
620 display: block;
621 }
622
623 @keyframes slideDown {
624 from {
625 opacity: 0;
626 max-height: 0;
627 transform: translateY(-10px);
628 }
629 to {
630 opacity: 1;
631 max-height: 100px;
632 transform: translateY(0);
633 }
634 }
635
636 /* Loading spinner animation */
637 @keyframes rotate {
638 from {
639 transform: rotate(0deg);
640 }
641 to {
642 transform: rotate(360deg);
643 }
644 }
645
646 /* Error popup specific styles */
647 .king-addons-translator-popup .king-addons-error-icon {
648 width: 60px;
649 height: 60px;
650 background: #f44336;
651 border-radius: 50%;
652 margin: 0 auto 16px;
653 display: flex;
654 align-items: center;
655 justify-content: center;
656 animation: errorPulse 2s ease-in-out infinite;
657 }
658
659 @keyframes errorPulse {
660 0%, 100% {
661 transform: scale(1);
662 box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4);
663 }
664 50% {
665 transform: scale(1.05);
666 box-shadow: 0 0 0 8px rgba(244, 67, 54, 0.1);
667 }
668 }
669
670 .king-addons-translator-actions {
671 display: flex;
672 gap: 12px;
673 position: sticky;
674 bottom: calc(var(--ka-tr-pad) * -1);
675 margin: 8px calc(var(--ka-tr-pad) * -1) calc(var(--ka-tr-pad) * -1);
676 padding: 14px var(--ka-tr-pad) var(--ka-tr-pad);
677 background: var(--ka-tr-surface);
678 border-top: 1px solid var(--ka-tr-border);
679 }
680
681 .king-addons-translator-btn-primary,
682 .king-addons-translator-btn-secondary {
683 padding: 11px 20px;
684 border-radius: var(--ka-tr-radius-sm);
685 font-size: 14px;
686 font-weight: 600;
687 font-family: inherit;
688 line-height: 1.2;
689 cursor: pointer;
690 flex: 1;
691 transition: background-color 0.15s ease, border-color 0.15s ease;
692 }
693
694 .king-addons-translator-btn-primary {
695 background: var(--ka-tr-accent);
696 border: 1px solid var(--ka-tr-accent);
697 color: #fff;
698 }
699
700 .king-addons-translator-btn-primary:hover {
701 background: var(--ka-tr-accent-hover);
702 border-color: var(--ka-tr-accent-hover);
703 color: #fff;
704 }
705
706 .king-addons-translator-btn-primary:disabled {
707 background: var(--ka-tr-border-strong);
708 border-color: var(--ka-tr-border-strong);
709 color: #fff;
710 cursor: not-allowed;
711 }
712
713 .king-addons-translator-btn-secondary {
714 background: var(--ka-tr-surface);
715 border: 1px solid var(--ka-tr-border-strong);
716 color: var(--ka-tr-ink);
717 }
718
719 .king-addons-translator-btn-secondary:hover {
720 background: var(--ka-tr-surface-sunken);
721 }
722
723 .king-addons-translator-btn-primary:focus-visible,
724 .king-addons-translator-btn-secondary:focus-visible {
725 outline: 2px solid var(--ka-tr-accent);
726 outline-offset: 2px;
727 }
728
729 /* Progress Styles */
730 .king-addons-translator-progress {
731 margin-top: 16px;
732 padding: 16px;
733 background: var(--ka-tr-surface-sunken);
734 border: 1px solid var(--ka-tr-border);
735 border-radius: var(--ka-tr-radius-sm);
736 }
737
738 .king-addons-translator-progress-text {
739 font-size: 14px;
740 color: #555;
741 margin-bottom: 8px;
742 }
743
744 .king-addons-translator-progress-bar {
745 width: 100%;
746 height: 6px;
747 background: var(--ka-tr-border);
748 border-radius: 999px;
749 overflow: hidden;
750 margin-bottom: 8px;
751 }
752
753 .king-addons-translator-progress-fill {
754 height: 100%;
755 background: var(--ka-tr-accent);
756 width: 0%;
757 transition: width 0.3s ease;
758 }
759
760 .king-addons-translator-current-element {
761 font-size: 12px;
762 color: var(--ka-tr-ink-muted);
763 }
764
765 .ka-tr-activity {
766 display: flex;
767 align-items: center;
768 gap: 8px;
769 min-height: 18px;
770 }
771
772 .ka-tr-spinner {
773 flex: 0 0 13px;
774 width: 13px;
775 height: 13px;
776 border: 2px solid var(--ka-tr-border);
777 border-top-color: var(--ka-tr-accent);
778 border-radius: 50%;
779 animation: rotate 0.7s linear infinite;
780 }
781
782 /* Respect a reduced-motion preference rather than spinning regardless. */
783 @media (prefers-reduced-motion: reduce) {
784 .ka-tr-spinner {
785 animation-duration: 2.4s;
786 }
787 }
788
789 .ka-tr-snippet {
790 margin-top: 8px;
791 padding: 8px 10px;
792 background: var(--ka-tr-surface);
793 border: 1px solid var(--ka-tr-border);
794 border-radius: var(--ka-tr-radius-sm);
795 font-size: 12px;
796 line-height: 1.45;
797 color: var(--ka-tr-ink-muted);
798 display: -webkit-box;
799 -webkit-line-clamp: 2;
800 -webkit-box-orient: vertical;
801 overflow: hidden;
802 }
803
804 .king-addons-translator-progress-note {
805 display: none;
806 margin-top: 10px;
807 padding: 10px 12px;
808 background: var(--ka-tr-warning-soft);
809 border: 1px solid var(--ka-tr-warning-border);
810 border-radius: var(--ka-tr-radius-sm);
811 color: var(--ka-tr-warning);
812 font-size: 12px;
813 line-height: 1.5;
814 }
815
816 /* Stats Styles */
817 .king-addons-translator-stats {
818 margin-top: 16px;
819 display: grid;
820 grid-template-columns: repeat(3, 1fr);
821 gap: 12px;
822 }
823
824 /* Shared dialog building blocks */
825 .ka-tr-dialog-head {
826 margin-bottom: 20px;
827 }
828
829 .ka-tr-dialog-head h3 {
830 margin: 0 0 6px 0;
831 }
832
833 .ka-tr-dialog-sub {
834 margin: 0;
835 font-size: 13px;
836 color: var(--ka-tr-ink-muted);
837 }
838
839 /* Says whose feature this is - inside Elementor's editor the
840 dialog otherwise reads as one of Elementor's own. */
841 .ka-tr-byline {
842 margin: -2px 0 12px;
843 font-size: 11px;
844 font-weight: 700;
845 letter-spacing: .08em;
846 text-transform: uppercase;
847 color: var(--ka-tr-accent);
848 }
849
850 .ka-tr-panel {
851 background: var(--ka-tr-surface-sunken);
852 border: 1px solid var(--ka-tr-border);
853 border-radius: var(--ka-tr-radius-sm);
854 padding: 16px;
855 margin-bottom: 12px;
856 }
857
858 .ka-tr-panel--accent {
859 background: var(--ka-tr-accent-soft);
860 border-color: rgba(91, 3, 255, 0.18);
861 }
862
863 .ka-tr-panel--warning {
864 background: var(--ka-tr-warning-soft);
865 border-color: var(--ka-tr-warning-border);
866 color: var(--ka-tr-warning);
867 }
868
869 .ka-tr-panel h4 {
870 margin: 0 0 10px 0;
871 font-size: 13px;
872 font-weight: 650;
873 color: var(--ka-tr-ink);
874 text-transform: uppercase;
875 letter-spacing: 0.04em;
876 }
877
878 .ka-tr-panel p {
879 margin: 0 0 12px 0;
880 font-size: 13px;
881 color: var(--ka-tr-ink-muted);
882 }
883
884 .ka-tr-panel p:last-child {
885 margin-bottom: 0;
886 }
887
888 .ka-tr-steps {
889 list-style: none;
890 counter-reset: ka-tr-step;
891 margin: 0;
892 padding: 0;
893 }
894
895 .ka-tr-steps li {
896 counter-increment: ka-tr-step;
897 position: relative;
898 padding-left: 28px;
899 margin: 0 0 10px 0;
900 font-size: 13px;
901 color: var(--ka-tr-ink);
902 line-height: 1.5;
903 }
904
905 .ka-tr-steps li:last-child {
906 margin-bottom: 0;
907 }
908
909 .ka-tr-steps li::before {
910 content: counter(ka-tr-step);
911 position: absolute;
912 left: 0;
913 top: 0;
914 width: 20px;
915 height: 20px;
916 border-radius: 50%;
917 background: var(--ka-tr-accent);
918 color: #fff;
919 font-size: 11px;
920 font-weight: 650;
921 display: flex;
922 align-items: center;
923 justify-content: center;
924 }
925
926 .ka-tr-choices {
927 list-style: none;
928 margin: 8px 0 0;
929 padding: 0;
930 display: grid;
931 gap: 6px;
932 }
933
934 .ka-tr-choices li {
935 font-size: 13px;
936 color: var(--ka-tr-ink-muted);
937 line-height: 1.5;
938 }
939
940 .ka-tr-steps a,
941 .ka-tr-panel a {
942 color: var(--ka-tr-accent);
943 font-weight: 600;
944 text-decoration: none;
945 }
946
947 .ka-tr-steps a:hover,
948 .ka-tr-panel a:hover {
949 text-decoration: underline;
950 }
951
952 .ka-tr-rows {
953 display: grid;
954 gap: 8px;
955 }
956
957 .ka-tr-row {
958 display: flex;
959 justify-content: space-between;
960 gap: 12px;
961 font-size: 13px;
962 }
963
964 .ka-tr-row span {
965 color: var(--ka-tr-ink-muted);
966 }
967
968 .ka-tr-row strong {
969 color: var(--ka-tr-ink);
970 font-weight: 600;
971 }
972
973 .ka-tr-detail {
974 font-size: 12px;
975 color: var(--ka-tr-ink-muted);
976 line-height: 1.5;
977 word-break: break-word;
978 max-height: 120px;
979 overflow-y: auto;
980 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
981 }
982
983 .king-addons-translator-stat {
984 text-align: center;
985 padding: 14px 12px;
986 background: var(--ka-tr-surface-sunken);
987 border: 1px solid var(--ka-tr-border);
988 border-radius: var(--ka-tr-radius-sm);
989 }
990
991 .king-addons-translator-stat-number {
992 font-size: 26px;
993 font-weight: 650;
994 line-height: 1.1;
995 letter-spacing: -0.02em;
996 color: var(--ka-tr-ink);
997 }
998
999 .king-addons-translator-stat-label {
1000 margin-top: 4px;
1001 font-size: 11px;
1002 font-weight: 600;
1003 text-transform: uppercase;
1004 letter-spacing: 0.05em;
1005 color: var(--ka-tr-ink-muted);
1006 }
1007
1008 .king-addons-translator-stat-number {
1009 font-size: 20px;
1010 font-weight: bold;
1011 color: #5B03FF;
1012 }
1013
1014 .king-addons-translator-stat-label {
1015 font-size: 12px;
1016 color: #777;
1017 margin-top: 4px;
1018 }
1019
1020 /* Element animations handled in preview iframe */
1021 </style>
1022 `;
1023 $('head').append(styles);
1024 }
1025 }
1026
1027 /**
1028 * Add translator button to Elementor panel
1029 */
1030 function addTranslatorButton() {
1031 // Check if button already exists
1032 if (document.querySelector('.king-addons-ai-translator-btn')) {
1033 return;
1034 }
1035
1036 // Strategy 1: Try to find the left button group in the top toolbar (after initial buttons)
1037 var $leftButtonGroup = $('#elementor-editor-wrapper-v2 .MuiStack-root.eui-1g5sxhh:first');
1038 if ($leftButtonGroup.length) {
1039 return addButtonToElement($leftButtonGroup, 'left-group');
1040 }
1041
1042 // Strategy 2: Try to find the first stack group in the toolbar
1043 var $toolbarStack = $('#elementor-editor-wrapper-v2 .MuiStack-root');
1044 if ($toolbarStack.length) {
1045 return addButtonToElement($toolbarStack.first(), 'toolbar-stack');
1046 }
1047
1048 // Strategy 2.5: Try to find grid container with stacks
1049 var $gridContainer = $('#elementor-editor-wrapper-v2 .MuiGrid-container:first');
1050 if ($gridContainer.length) {
1051 var $firstStack = $gridContainer.find('.MuiStack-root:first');
1052 if ($firstStack.length) {
1053 return addButtonToElement($firstStack, 'grid-stack');
1054 }
1055 }
1056
1057 // Strategy 3: Try to find the toolbar itself
1058 var $toolbar = $('#elementor-editor-wrapper-v2 .MuiToolbar-root');
1059 if ($toolbar.length) {
1060 return addButtonToElement($toolbar, 'toolbar');
1061 }
1062
1063 // Strategy 4: Try to find the Elementor panel header (fallback)
1064 var $panelHeader = $('#elementor-panel-header');
1065 if ($panelHeader.length) {
1066 return addButtonToElement($panelHeader, 'panel-header');
1067 }
1068
1069 // Strategy 5: Try to find the main panel (further fallback)
1070 var $panel = $('#elementor-panel');
1071 if ($panel.length) {
1072 return addButtonToElement($panel, 'panel-fallback');
1073 }
1074 return false;
1075 }
1076
1077 /**
1078 * Helper function to add button to specific element
1079 */
1080 function addButtonToElement($target, location) {
1081 // Try using the custom icon, fallback to the standard AI icon
1082 var iconUrl = KingAddonsAiField.plugin_url + 'includes/admin/img/ai.svg';
1083 var fallbackIconUrl = KingAddonsAiField.plugin_url + 'includes/admin/img/ai.svg';
1084
1085 var $translatorBtn;
1086
1087 // Create button with appropriate styling based on location
1088 if (location === 'left-group' || location === 'toolbar-stack' || location === 'toolbar' || location === 'grid-stack') {
1089 // Material UI style button for toolbar with text and custom icon
1090 $translatorBtn = $('<span class="MuiBox-root eui-0">' +
1091 '<button class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textInherit MuiButton-sizeSmall MuiButton-textSizeSmall MuiButton-colorInherit king-addons-ai-translator-btn eui-17yw4pm" ' +
1092 'tabindex="0" type="button" aria-label="AI Page Translate & Transform" title="AI Page Translate & Transform">' +
1093 '<span class="MuiButton-startIcon MuiButton-iconSizeSmall" style="margin-right: 4px;">' +
1094 '<img src="' + iconUrl + '" alt="AI" onerror="this.src=\'' + fallbackIconUrl + '\'" style="width: 20px; height: 20px;" />' +
1095 '</span>' +
1096 '<span class="MuiStack-root" style="color: white;">AI Page Translate &amp; Transform</span>' +
1097 '</button>' +
1098 '</span>');
1099 } else {
1100 // Original button style for panel locations
1101 $translatorBtn = $('<button class="king-addons-ai-translator-btn" title="AI Page Translate & Transform">' +
1102 '<img src="' + iconUrl + '" alt="" onerror="this.src=\'' + fallbackIconUrl + '\'"/>' +
1103 '<span>AI Page Translate &amp; Transform</span>' +
1104 '</button>');
1105 }
1106
1107 // Add location-specific class for different styling if needed
1108 $translatorBtn.addClass('king-addons-translator-location-' + location);
1109
1110 // Add to the target element based on location
1111 if (location === 'panel-top' || location === 'panel-fallback') {
1112 $target.prepend($translatorBtn);
1113 } else if (location === 'left-group' || location === 'toolbar-stack' || location === 'grid-stack') {
1114 // Add after existing buttons in the left group
1115 $target.append($translatorBtn);
1116 } else {
1117 $target.append($translatorBtn);
1118 }
1119
1120 // Bind click event (works for both button structures)
1121 $translatorBtn.find('button').length ?
1122 $translatorBtn.find('button').on('click', handleButtonClick) :
1123 $translatorBtn.on('click', handleButtonClick);
1124
1125 function handleButtonClick(e) {
1126 e.preventDefault();
1127 // Check if button is disabled or translation is in progress
1128 if (translationState.isTranslating || $(e.currentTarget).prop('disabled')) {
1129 return;
1130 }
1131 showTranslatorPopup();
1132 }
1133
1134 // Add debug info to button
1135 var $btn = $translatorBtn.find('button').length ? $translatorBtn.find('button') : $translatorBtn;
1136 $btn.attr('data-location', location);
1137 $btn.attr('data-target', $target.prop('tagName') + ($target.attr('id') ? '#' + $target.attr('id') : '') + ($target.attr('class') ? '.' + $target.attr('class').split(' ').join('.') : ''));
1138
1139 return true;
1140 }
1141
1142 /**
1143 * Show the translator popup
1144 */
1145 function showTranslatorPopup() {
1146 // Check API key first
1147 checkApiKeyAndShowPopup();
1148 }
1149
1150 /**
1151 * Check API key before showing popup
1152 */
1153 function checkApiKeyAndShowPopup() {
1154 // Show loading state briefly
1155 var $loadingOverlay = showLoadingOverlay();
1156
1157 $.post(KingAddonsAiField.ajax_url, {
1158 action: 'king_addons_ai_check_tokens',
1159 nonce: KingAddonsAiField.generate_nonce
1160 }, function(response) {
1161 $loadingOverlay.remove();
1162
1163 if (!response.success) {
1164 if (response.data && response.data.message) {
1165 var errorMessage = response.data.message;
1166
1167 // Check for token limit errors first
1168 if (errorMessage.toLowerCase().includes('token limit') ||
1169 errorMessage.toLowerCase().includes('daily limit') ||
1170 errorMessage.toLowerCase().includes('limit reached') ||
1171 errorMessage.toLowerCase().includes('quota exceeded') ||
1172 errorMessage.toLowerCase().includes('rate limit')) {
1173
1174 showTokenLimitError(errorMessage);
1175 return;
1176 }
1177
1178 showApiKeyError('API Error', errorMessage);
1179 } else {
1180 showApiKeyError('Connection Issue', 'Unable to connect right now. Please check your internet connection and try again.');
1181 }
1182 return;
1183 }
1184
1185 if (!response.data.api_key_valid) {
1186 var errorMessage = response.data.error_message || 'API key is missing or invalid';
1187
1188 // Check for token limit errors first
1189 if (errorMessage.toLowerCase().includes('token limit') ||
1190 errorMessage.toLowerCase().includes('daily limit') ||
1191 errorMessage.toLowerCase().includes('limit reached') ||
1192 errorMessage.toLowerCase().includes('quota exceeded') ||
1193 errorMessage.toLowerCase().includes('rate limit') ||
1194 errorMessage.toLowerCase().includes('too many requests')) {
1195
1196 showTokenLimitError(errorMessage);
1197 return;
1198 }
1199
1200 showApiKeyError('API Key Required', errorMessage);
1201 return;
1202 }
1203
1204 var saved = loadTranslationProgress();
1205 if (saved) {
1206 showResumePopup(saved);
1207 return;
1208 }
1209
1210 createAndShowPopup();
1211 }).fail(function(xhr) {
1212 $loadingOverlay.remove();
1213
1214 if (xhr.status === 0) {
1215 showApiKeyError('Connection Issue', 'Network connection failed. Please check your internet connection and try again.');
1216 } else {
1217 showApiKeyError('Temporary Issue', 'Server is temporarily unavailable. Please try again in a few minutes.');
1218 }
1219 });
1220 }
1221
1222 /**
1223 * Show loading overlay
1224 */
1225 function showLoadingOverlay() {
1226 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1227 var $popup = $('<div class="king-addons-translator-popup" style="text-align: center; padding: 40px;"></div>');
1228
1229 var loadingHtml = `
1230 <div style="margin-bottom: 16px;">
1231 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:40px;height:40px;filter: invert(1); animation: rotate 1s linear infinite;"/>
1232 </div>
1233 <div style="font-size: 16px; color: #333; margin-bottom: 8px;">🔍 Verifying Setup...</div>
1234 <div style="font-size: 12px; color: #666;">Just checking that everything is ready for translation</div>
1235 `;
1236
1237 $popup.html(loadingHtml);
1238 $overlay.append($popup);
1239 $('body').append($overlay);
1240
1241 return $overlay;
1242 }
1243
1244 /**
1245 * Show API key error with detailed information
1246 */
1247 function showApiKeyError(title, message) {
1248 // Aggressively remove any existing popups/overlays
1249 $('.king-addons-translator-overlay').remove();
1250 $('.king-addons-translator-popup').remove();
1251
1252 // Wait a bit to ensure cleanup is complete
1253 setTimeout(function() {
1254 showApiKeyErrorDelayed(title, message);
1255 }, 100);
1256 }
1257
1258 function showApiKeyErrorDelayed(title, message) {
1259 var cfg = window.KingAddonsAiField || {};
1260 var settingsUrl = cfg.settings_url || '/wp-admin/admin.php?page=king-addons-ai-settings';
1261
1262 // The setup steps name whichever AI provider is configured.
1263 var keysUrl = cfg.api_keys_url || 'https://platform.openai.com/api-keys';
1264 var keysLabel = cfg.api_keys_label || 'OpenAI Platform';
1265 var billingNote = cfg.setup_billing_note || 'and top up your OpenAI account balance by at least $5';
1266 var costNote = cfg.setup_cost_note || 'Processing a page costs pennies (about $0.01 per full page).';
1267
1268 // With no key stored for either provider there is nothing configured to
1269 // describe, so the dialog offers the choice rather than assuming one.
1270 // wp_localize_script stringifies booleans, so false arrives as '' - test
1271 // for falsiness rather than for the boolean itself.
1272 var hasAnyKey = cfg.has_any_key;
1273 var isFirstRun = (hasAnyKey === false || hasAnyKey === '' || hasAnyKey === '0' || hasAnyKey === 0)
1274 && !!(cfg.setup_providers && cfg.setup_providers.length);
1275
1276 function esc(value) {
1277 return $('<div></div>').text(String(value == null ? '' : value)).html();
1278 }
1279
1280 function providerChoiceHtml() {
1281 var items = cfg.setup_providers.map(function (provider) {
1282 return '<li><a href="' + esc(provider.url) + '" target="_blank" rel="noopener noreferrer">'
1283 + esc(provider.name) + '</a> &mdash; ' + esc(provider.note) + '</li>';
1284 }).join('');
1285 return '<ul class="ka-tr-choices">' + items + '</ul>';
1286 }
1287
1288 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1289 var $popup = $('<div class="king-addons-translator-popup"></div>');
1290
1291 var errorHtml = `
1292 <div class="ka-tr-dialog-head">
1293 <h3>${esc(title || 'AI Page Translate & Transform')}</h3>
1294 <div class="ka-tr-byline">by King Addons</div>
1295 <p class="ka-tr-dialog-sub">Connect an AI provider once and the feature is ready to use.</p>
1296 </div>
1297
1298 <div class="ka-tr-panel">
1299 <h4>What you need to do</h4>
1300 <ol class="ka-tr-steps">
1301 ${isFirstRun
1302 ? `<li>Pick a provider and get an API key:${providerChoiceHtml()}</li>`
1303 : `<li>Get an API key from <a href="${esc(keysUrl)}" target="_blank" rel="noopener noreferrer">${esc(keysLabel)}</a> ${esc(billingNote)}</li>`}
1304 <li>Paste it into AI Settings${isFirstRun ? ', choosing the same provider there' : ''}</li>
1305 <li>Come back here and run it on your page</li>
1306 </ol>
1307 </div>
1308
1309 ${message ? `<div class="ka-tr-panel"><h4>Details</h4><div class="ka-tr-detail">${esc(message)}</div></div>` : ''}
1310
1311 <div class="ka-tr-panel">
1312 <h4>What it costs</h4>
1313 <p>${esc(isFirstRun ? (cfg.setup_cost_note_neutral || costNote) : costNote)}</p>
1314 </div>
1315
1316 <div class="king-addons-translator-actions">
1317 <button class="king-addons-translator-btn-secondary" id="king-addons-error-close">Not now</button>
1318 <a href="${esc(settingsUrl)}" class="king-addons-translator-btn-primary" style="text-decoration: none; display: flex; align-items: center; justify-content: center;">Go to AI Settings</a>
1319 </div>
1320 `;
1321
1322 $popup.html(errorHtml);
1323 $overlay.append($popup);
1324 $('body').append($overlay);
1325
1326 // Bind close event
1327 $('#king-addons-error-close').on('click', function() {
1328 $overlay.remove();
1329 });
1330
1331 // Close on overlay click
1332 $overlay.on('click', function(e) {
1333 if (e.target === $overlay[0]) {
1334 $overlay.remove();
1335 }
1336 });
1337 }
1338
1339 /**
1340 * Show token limit error popup
1341 */
1342 function showTokenLimitError(message, errorCode) {
1343 // Remove any existing popups first
1344 $('.king-addons-translator-overlay').remove();
1345 $('.king-addons-translator-popup').remove();
1346
1347 // Wait a bit to ensure cleanup is complete
1348 setTimeout(function() {
1349 showTokenLimitErrorDelayed(message, errorCode);
1350 }, 100);
1351 }
1352
1353 /**
1354 * A run can be stopped by four different limits, and they need four
1355 * different answers: the plugin's own token cap, the provider's short-term
1356 * throttling, a per-model daily cap, and an empty account balance. Showing
1357 * "increase your Daily Token Limit" for all of them sends people to a
1358 * setting that has nothing to do with the failure.
1359 */
1360 function showTokenLimitErrorDelayed(message, errorCode) {
1361 var cfg = window.KingAddonsAiField || {};
1362 var settingsUrl = cfg.settings_url || '/wp-admin/admin.php?page=king-addons-ai-settings';
1363 var providerLabel = cfg.provider_label || 'the AI provider';
1364 var isOpenRouter = cfg.provider === 'openrouter';
1365
1366 function esc(value) {
1367 return $('<div></div>').text(String(value == null ? '' : value)).html();
1368 }
1369
1370 var variants = {
1371 local_limit: {
1372 title: 'Daily token limit reached',
1373 subtitle: 'Your own safety limit stopped the translation.',
1374 heading: 'What happened',
1375 body: 'King Addons has a <strong>"Daily Token Limit"</strong> setting that prevents accidental '
1376 + 'overspending, and this page hit it. Nothing is wrong with your ' + esc(providerLabel) + ' account.',
1377 steps: [
1378 '<strong>Increase the "Daily Token Limit"</strong> in AI Settings (recommended)',
1379 'Or wait until tomorrow &mdash; the limit resets automatically'
1380 ],
1381 tip: 'Go to <strong>AI Settings → Daily Token Limit</strong> and set a higher number. '
1382 + 'For regular use, try <strong>50,000 or 100,000 tokens</strong>.'
1383 },
1384 rate_limit: {
1385 title: 'Model rate limit reached',
1386 subtitle: esc(providerLabel) + ' is throttling requests for the selected model.',
1387 heading: 'What happened',
1388 body: 'The model was asked for translations faster than the provider allows, and it kept '
1389 + 'refusing after several retries. This is a temporary limit, not a problem with your account.',
1390 steps: [
1391 'Wait a minute and resume &mdash; the limit clears on its own',
1392 'Or pick a less busy model in AI Settings'
1393 ].concat(isOpenRouter ? ['Free models share a pool with other users; a paid model has far higher limits'] : []),
1394 tip: 'Your progress was saved. Reopen the AI Translator and choose <strong>Resume</strong> '
1395 + 'to continue from where it stopped.'
1396 },
1397 daily_limit: {
1398 title: 'Daily model limit reached',
1399 subtitle: esc(providerLabel) + ' has capped this model for today.',
1400 heading: 'What happened',
1401 body: 'The selected model has a daily request cap and it has been used up. Waiting a few '
1402 + 'seconds will not help &mdash; the cap resets on the provider\'s schedule.',
1403 steps: [
1404 'Switch to a different model in AI Settings',
1405 'Or come back after the cap resets'
1406 ].concat(isOpenRouter ? ['Free models have daily caps that credits do not lift; a paid model avoids them'] : []),
1407 tip: 'Your progress was saved. Reopen the AI Translator and choose <strong>Resume</strong> '
1408 + 'to continue from where it stopped.'
1409 },
1410 credits: {
1411 title: 'Out of credits',
1412 subtitle: 'Your ' + esc(providerLabel) + ' account has no balance left.',
1413 heading: 'What happened',
1414 body: esc(providerLabel) + ' rejected the request because the account balance is empty. '
1415 + 'The plugin and your API key are fine.',
1416 steps: isOpenRouter
1417 ? ['Add credit at <a href="https://openrouter.ai/settings/credits" target="_blank" rel="noopener noreferrer" style="color:#5B03FF;">openrouter.ai/settings/credits</a>',
1418 'Or switch to a free model in AI Settings']
1419 : ['Top up your account balance in the provider dashboard',
1420 'Then run the translation again'],
1421 tip: 'Your progress was saved. Reopen the AI Translator and choose <strong>Resume</strong> '
1422 + 'to continue from where it stopped.'
1423 }
1424 };
1425
1426 var variant = variants[errorCode] || variants.local_limit;
1427
1428 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1429 var $popup = $('<div class="king-addons-translator-popup"></div>');
1430
1431 var stepsHtml = variant.steps.map(function(step) {
1432 return '<li>' + step + '</li>';
1433 }).join('');
1434
1435 // The provider's own wording is the most precise explanation there is,
1436 // so it is shown verbatim rather than paraphrased away.
1437 var detailHtml = message ? `
1438 <div class="ka-tr-panel">
1439 <h4>Provider response</h4>
1440 <div class="ka-tr-detail">${esc(message)}</div>
1441 </div>` : '';
1442
1443 $popup.html(`
1444 <div class="ka-tr-dialog-head">
1445 <h3>${variant.title}</h3>
1446 <p class="ka-tr-dialog-sub">${variant.subtitle}</p>
1447 </div>
1448
1449 <div class="ka-tr-panel">
1450 <h4>${variant.heading}</h4>
1451 <p>${variant.body}</p>
1452 </div>
1453
1454 <div class="ka-tr-panel ka-tr-panel--accent">
1455 <h4>What to do</h4>
1456 <ol class="ka-tr-steps">${stepsHtml}</ol>
1457 </div>
1458
1459 ${detailHtml}
1460
1461 <div class="ka-tr-panel ka-tr-panel--warning">
1462 <p>${variant.tip}</p>
1463 </div>
1464
1465 <div class="king-addons-translator-actions">
1466 <button class="king-addons-translator-btn-secondary" id="king-addons-limit-close">I understand</button>
1467 <a href="${esc(settingsUrl)}" class="king-addons-translator-btn-primary" style="text-decoration: none; display: flex; align-items: center; justify-content: center;">Go to AI Settings</a>
1468 </div>
1469 `);
1470
1471 $overlay.append($popup);
1472 $('body').append($overlay);
1473
1474 $('#king-addons-limit-close').on('click', function() {
1475 $overlay.remove();
1476 });
1477
1478 $overlay.on('click', function(e) {
1479 if (e.target === $overlay[0]) {
1480 $overlay.remove();
1481 }
1482 });
1483 }
1484
1485 function toggleTranslatorButton(disabled) {
1486 var $button = $('.king-addons-ai-translator-btn');
1487
1488 if (disabled) {
1489 $button.prop('disabled', true);
1490 $button.css('opacity', '0.5');
1491 $button.css('cursor', 'not-allowed');
1492 } else {
1493 $button.prop('disabled', false);
1494 $button.css('opacity', '1');
1495 $button.css('cursor', 'pointer');
1496 }
1497 }
1498
1499 /**
1500 * Stop the translation process
1501 */
1502 function stopTranslationProcess() {
1503 // Prevent multiple calls
1504 if (translationState.isCancelled) {
1505 return;
1506 }
1507
1508 translationState.isCancelled = true;
1509 translationState.isTranslating = false;
1510
1511 // Cancel all active AJAX requests
1512 if (translationState.currentRequests.length > 0) {
1513 translationState.currentRequests.forEach(function(request) {
1514 if (request && request.abort) {
1515 request.abort();
1516 }
1517 });
1518 translationState.currentRequests = [];
1519 }
1520
1521 // Remove any highlighting from current element
1522 if (translationState.currentElement) {
1523 highlightElementInPreview(translationState.currentElement.elementId, false);
1524 }
1525
1526 // Remove any existing popups/overlays
1527 $('.king-addons-translator-overlay').remove();
1528
1529 // Re-enable the button
1530 toggleTranslatorButton(false);
1531 }
1532
1533 /**
1534 * Animate popup to top-right corner
1535 */
1536 function movePopupToCorner($popup, $overlay) {
1537 return new Promise(function(resolve) {
1538 // Add moving class for smooth animation
1539 $popup.addClass('moving');
1540
1541 // Hide overlay with fade
1542 $overlay.addClass('hiding');
1543
1544 // Calculate current position and target position
1545 var currentRect = $popup[0].getBoundingClientRect();
1546 var targetTop = 80;
1547 var targetRight = 20;
1548 var targetLeft = window.innerWidth - 350 - 20;
1549
1550 // Move popup from overlay to body with current position
1551 $popup.css({
1552 'position': 'fixed',
1553 'top': currentRect.top + 'px',
1554 'left': currentRect.left + 'px',
1555 'width': currentRect.width + 'px',
1556 'margin': '0',
1557 'transform': 'none',
1558 'z-index': 999999
1559 });
1560
1561 // Append popup to body (remove from overlay)
1562 $('body').append($popup);
1563
1564 // Wait for overlay to fade, then animate popup
1565 setTimeout(function() {
1566 // Force reflow
1567 $popup[0].offsetHeight;
1568
1569 // Animate to final position
1570 $popup.css({
1571 'top': targetTop + 'px',
1572 'left': targetLeft + 'px',
1573 'width': '350px',
1574 'padding': '16px'
1575 });
1576
1577 // Add compact class after animation and remove overlay
1578 setTimeout(function() {
1579 $popup.removeClass('moving').addClass('compact');
1580 $overlay.remove(); // Remove overlay completely
1581 resolve();
1582 }, 500);
1583
1584 }, 300);
1585 });
1586 }
1587
1588 /**
1589 * Create and show the main popup
1590 */
1591 /**
1592 * Human readable name for a language code or a custom prompt.
1593 */
1594 function describeLanguage(code) {
1595 return languages[code] || code || 'the target language';
1596 }
1597
1598 /**
1599 * Offer to continue an interrupted run instead of starting over.
1600 *
1601 * Elementor keeps translated content as unsaved changes, so a reload only
1602 * preserves it once the document has been saved or autosaved - the prompt
1603 * says so rather than pretending otherwise.
1604 */
1605 function showResumePopup(saved) {
1606 $('.king-addons-translator-overlay').remove();
1607
1608 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1609 var $popup = $('<div class="king-addons-translator-popup"></div>');
1610
1611 var remaining = Math.max(0, saved.total - saved.done.length);
1612
1613 function esc(value) {
1614 return $('<div></div>').text(String(value == null ? '' : value)).html();
1615 }
1616
1617 // Mirrors the main popup's skeleton (h3, a subtitle sibling, and a
1618 // .king-addons-translator-form body) so showProgressInPopup() can take
1619 // it over once the run starts.
1620 $popup.html(`
1621 <h3>
1622 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);" alt=""/>
1623 Resume this run?
1624 </h3>
1625 <div class="ka-tr-byline">by King Addons</div>
1626 <div class="ka-tr-dialog-sub" style="margin-bottom: 16px;">
1627 A run on this page was interrupted.
1628 </div>
1629 <div class="king-addons-translator-form">
1630 <div class="ka-tr-panel">
1631 <div class="ka-tr-rows">
1632 <div class="ka-tr-row"><span>Progress</span><strong>${esc(saved.done.length)} / ${esc(saved.total)} elements</strong></div>
1633 <div class="ka-tr-row"><span>Remaining</span><strong>${esc(remaining)} elements</strong></div>
1634 <div class="ka-tr-row"><span>Translating into</span><strong>${esc(describeLanguage(saved.toLang))}</strong></div>
1635 </div>
1636 </div>
1637
1638 <div class="ka-tr-panel ka-tr-panel--warning">
1639 <p>
1640 Resuming skips the elements that were already done. If the page was reloaded
1641 without saving, those elements kept their original text &mdash; choose
1642 <strong>Start over</strong> to translate the whole page again.
1643 </p>
1644 </div>
1645
1646 <div class="king-addons-translator-actions">
1647 <button class="king-addons-translator-btn-secondary" id="king-addons-resume-discard">Start over</button>
1648 <button class="king-addons-translator-btn-primary" id="king-addons-resume-continue">Resume</button>
1649 </div>
1650 </div>
1651 `);
1652
1653 $overlay.append($popup);
1654 $('body').append($overlay);
1655
1656 // Hand the same popup to the normal flow, which swaps its body for the
1657 // progress UI and animates it into the corner.
1658 $('#king-addons-resume-continue').on('click', function() {
1659 startTranslation(saved.fromLang || 'auto', saved.toLang, $popup, $overlay, saved);
1660 });
1661
1662 $('#king-addons-resume-discard').on('click', function() {
1663 clearTranslationProgress();
1664 $overlay.remove();
1665 createAndShowPopup();
1666 });
1667
1668 $overlay.on('click', function(e) {
1669 if (e.target === $overlay[0]) {
1670 $overlay.remove();
1671 }
1672 });
1673 }
1674
1675 /**
1676 * Small banner shown after the editor loads when a run can be continued.
1677 */
1678 function offerResumeOnLoad() {
1679 if (translationState.isTranslating || $('#king-addons-translator-resume-banner').length) {
1680 return;
1681 }
1682
1683 var saved = loadTranslationProgress();
1684 if (!saved) {
1685 return;
1686 }
1687
1688 var $banner = $(`
1689 <div id="king-addons-translator-resume-banner" style="position: fixed; bottom: 20px; right: 20px; z-index: 999998; max-width: 320px; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); padding: 16px; font-size: 13px; color: #2d3748;">
1690 <div style="font-weight: 600; margin-bottom: 6px;">Unfinished run</div>
1691 <div style="color: #718096; line-height: 1.5; margin-bottom: 12px;">
1692 ${saved.done.length} of ${saved.total} elements were processed into ${$('<div></div>').text(describeLanguage(saved.toLang)).html()}.
1693 </div>
1694 <div style="display: flex; gap: 8px;">
1695 <button type="button" id="king-addons-resume-banner-dismiss" style="flex: 1; border: 1px solid #e2e8f0; background: #f7fafc; color: #4a5568; border-radius: 6px; padding: 7px 10px; cursor: pointer;">Later</button>
1696 <button type="button" id="king-addons-resume-banner-open" style="flex: 1; border: none; background: #5B03FF; color: #fff; border-radius: 6px; padding: 7px 10px; cursor: pointer;">Resume</button>
1697 </div>
1698 </div>
1699 `);
1700
1701 $('body').append($banner);
1702
1703 $('#king-addons-resume-banner-open').on('click', function() {
1704 $banner.remove();
1705 var current = loadTranslationProgress();
1706 if (current) {
1707 showResumePopup(current);
1708 }
1709 });
1710
1711 // "Later" only hides the banner; the saved progress stays available
1712 // from the AI Translator button.
1713 $('#king-addons-resume-banner-dismiss').on('click', function() {
1714 $banner.remove();
1715 });
1716 }
1717
1718 function createAndShowPopup() {
1719 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1720 var $popup = $('<div class="king-addons-translator-popup"></div>');
1721
1722 var isPro = isPremiumActive();
1723 var customOptionHtml = isPro ?
1724 '<option value="custom">Custom language or prompt (PRO)</option>' :
1725 '<option value="custom" disabled>Custom language or prompt (PRO)</option>';
1726
1727 var upgradeUrl = 'https://kingaddons.com/pricing/?utm_source=ai-translator&utm_medium=plugin&utm_campaign=custom-prompts';
1728 var proInfoHtml = isPro ?
1729 '<div class="king-addons-pro-info" style="color: #4CAF50; border-left-color: #4CAF50;">�
1730 PRO Active: Use custom languages and translation prompts!</div>' :
1731 '<div class="king-addons-pro-info">💎 <a href="' + upgradeUrl + '" target="_blank" style="color: #5B03FF; text-decoration: none;">Upgrade to King Addons PRO</a> to use custom languages, regional dialects and custom translation prompts (formal tone, technical style, etc.)!</div>';
1732
1733 var popupContent = `
1734 <h3>
1735 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);" alt=""/>
1736 AI Page Translate &amp; Transform
1737 </h3>
1738 <div class="ka-tr-byline">by King Addons</div>
1739 <div class="ka-tr-dialog-sub" style="margin-bottom: 20px;">
1740 Translate to any language, or transform the text style (formal, casual, technical).
1741 </div>
1742 <div class="king-addons-translator-form">
1743 <div class="king-addons-translator-field">
1744 <label>From Language</label>
1745 <select id="king-addons-from-lang">
1746 <option value="auto">Auto-detect</option>
1747 ${Object.keys(languages).map(code =>
1748 `<option value="${code}">${languages[code]}</option>`
1749 ).join('')}
1750 ${customOptionHtml}
1751 </select>
1752 <div class="king-addons-custom-language-field" id="king-addons-custom-from-field">
1753 <label>Custom language or translation prompt</label>
1754 <input type="text" id="king-addons-custom-from-lang" placeholder="e.g., Klingon, Old English, formal business tone, medical terminology..." />
1755 <div class="king-addons-prompt-examples">
1756 <small>Examples: "Klingon", "Shakespeare English", "formal business style", "casual conversational tone"</small>
1757 </div>
1758 </div>
1759 </div>
1760 <div class="king-addons-translator-field">
1761 <label>To Language</label>
1762 <select id="king-addons-to-lang">
1763 ${Object.keys(languages).map(code =>
1764 `<option value="${code}" ${code === 'en' ? 'selected' : ''}>${languages[code]}</option>`
1765 ).join('')}
1766 ${customOptionHtml}
1767 </select>
1768 <div class="king-addons-custom-language-field" id="king-addons-custom-to-field">
1769 <label>Custom language or translation style</label>
1770 <input type="text" id="king-addons-custom-to-lang" placeholder="e.g., Dothraki, Academic writing, pirate speak, baby talk..." />
1771 <div class="king-addons-prompt-examples">
1772 <small>Examples: "Dothraki", "academic paper style", "pirate language", "simplified for children"</small>
1773 </div>
1774 </div>
1775 </div>
1776 ${proInfoHtml}
1777 <div class="king-addons-translator-actions">
1778 <button class="king-addons-translator-btn-secondary" id="king-addons-cancel-translation">
1779 Cancel
1780 </button>
1781 <button class="king-addons-translator-btn-primary" id="king-addons-start-translation">
1782 Start Translation
1783 </button>
1784 </div>
1785 </div>
1786 `;
1787
1788 $popup.html(popupContent);
1789 $overlay.append($popup);
1790 $('body').append($overlay);
1791
1792 // Store references globally
1793 window.currentTranslatorPopup = $popup;
1794 window.currentTranslatorOverlay = $overlay;
1795
1796 // Bind events for language selection
1797 bindLanguageSelectionEvents();
1798
1799 // Bind events
1800 $('#king-addons-cancel-translation').on('click', function() {
1801 if (!translationState.isTranslating) {
1802 $overlay.remove();
1803 }
1804 });
1805
1806 $('#king-addons-start-translation').on('click', function() {
1807 var result = getSelectedLanguages();
1808
1809 if (!result.valid) {
1810 alert(result.error);
1811 return;
1812 }
1813
1814 // Double-check API key before starting translation
1815 var $button = $(this);
1816 $button.prop('disabled', true).text('🔍 Verifying...');
1817
1818 $.post(KingAddonsAiField.ajax_url, {
1819 action: 'king_addons_ai_check_tokens',
1820 nonce: KingAddonsAiField.generate_nonce
1821 }, function(response) {
1822 $button.prop('disabled', false).text('Start Translation');
1823
1824 if (!response.success || !response.data.api_key_valid) {
1825 var errorMessage = 'API key verification failed. Please check your API key in settings.';
1826 if (response.data && response.data.error_message) {
1827 errorMessage = response.data.error_message;
1828 }
1829
1830 // Check for token limit errors first
1831 if (errorMessage.toLowerCase().includes('token limit') ||
1832 errorMessage.toLowerCase().includes('daily limit') ||
1833 errorMessage.toLowerCase().includes('limit reached') ||
1834 errorMessage.toLowerCase().includes('quota exceeded') ||
1835 errorMessage.toLowerCase().includes('rate limit') ||
1836 errorMessage.toLowerCase().includes('too many requests')) {
1837
1838 // Show token limit error popup
1839 showTokenLimitError(errorMessage);
1840 return;
1841 }
1842
1843 // Show error popup (will handle cleanup automatically)
1844 showApiKeyError('Setup Required', errorMessage);
1845 return;
1846 }
1847
1848 // API key is valid, proceed with translation
1849 startTranslation(result.fromLang, result.toLang, $popup, $overlay);
1850
1851 }).fail(function() {
1852 $button.prop('disabled', false).text('Start Translation');
1853
1854 // Show error popup (will handle cleanup automatically)
1855 showApiKeyError('Connection Issue', 'Failed to connect right now. Please check your connection and try again.');
1856 });
1857 });
1858
1859 // Close on overlay click only if not translating
1860 $overlay.on('click', function(e) {
1861 if (e.target === $overlay[0] && !translationState.isTranslating) {
1862 $overlay.remove();
1863 }
1864 });
1865 }
1866
1867 /**
1868 * Bind events for language selection dropdowns
1869 */
1870 function bindLanguageSelectionEvents() {
1871 // Handle From Language selection
1872 $('#king-addons-from-lang').on('change', function() {
1873 var selectedValue = $(this).val();
1874 var $customField = $('#king-addons-custom-from-field');
1875
1876 if (selectedValue === 'custom') {
1877 if (!isPremiumActive()) {
1878 // Reset to previous value and show upgrade message
1879 $(this).val('auto');
1880 alert('Custom languages and translation prompts are a PRO feature. Please upgrade to King Addons PRO to use custom languages or translation styles.');
1881 return;
1882 }
1883 $customField.addClass('show');
1884 $('#king-addons-custom-from-lang').focus();
1885 } else {
1886 $customField.removeClass('show');
1887 }
1888 });
1889
1890 // Handle To Language selection
1891 $('#king-addons-to-lang').on('change', function() {
1892 var selectedValue = $(this).val();
1893 var $customField = $('#king-addons-custom-to-field');
1894
1895 if (selectedValue === 'custom') {
1896 if (!isPremiumActive()) {
1897 // Reset to previous value and show upgrade message
1898 $(this).val('en');
1899 alert('Custom languages and translation prompts are a PRO feature. Please upgrade to King Addons PRO to use custom languages or translation styles.');
1900 return;
1901 }
1902 $customField.addClass('show');
1903 $('#king-addons-custom-to-lang').focus();
1904 } else {
1905 $customField.removeClass('show');
1906 }
1907 });
1908 }
1909
1910 /**
1911 * Get selected languages with validation
1912 */
1913 function getSelectedLanguages() {
1914 var fromLang = $('#king-addons-from-lang').val();
1915 var toLang = $('#king-addons-to-lang').val();
1916 var customFromLang = $('#king-addons-custom-from-lang').val().trim();
1917 var customToLang = $('#king-addons-custom-to-lang').val().trim();
1918
1919 // Handle custom from language
1920 if (fromLang === 'custom') {
1921 if (!customFromLang) {
1922 return {
1923 valid: false,
1924 error: 'Please enter a custom source language or translation prompt.'
1925 };
1926 }
1927 fromLang = customFromLang;
1928 }
1929
1930 // Handle custom to language
1931 if (toLang === 'custom') {
1932 if (!customToLang) {
1933 return {
1934 valid: false,
1935 error: 'Please enter a custom target language or translation style.'
1936 };
1937 }
1938 toLang = customToLang;
1939 }
1940
1941 // Validate languages are different (except auto-detect)
1942 if (fromLang === toLang && fromLang !== 'auto') {
1943 return {
1944 valid: false,
1945 error: 'Source and target languages cannot be the same.'
1946 };
1947 }
1948
1949 return {
1950 valid: true,
1951 fromLang: fromLang,
1952 toLang: toLang
1953 };
1954 }
1955
1956 /**
1957 * Start the translation process
1958 */
1959 function startTranslation(fromLang, toLang, $popup, $overlay, resumeFrom) {
1960 translationState.isTranslating = true;
1961 translationState.isCancelled = false; // Reset cancellation flag
1962 translationState.currentRequests = []; // Clear any previous requests
1963 translationState.fromLang = fromLang;
1964 translationState.toLang = toLang;
1965 translationState.translatedElements = 0;
1966 translationState.failedElements = 0;
1967 translationState.doneElementIds = [];
1968 translationState.failedElementIds = [];
1969 translationState.lastErrorMessage = '';
1970 translationState.consecutiveFailures = 0;
1971
1972 // Inject animation styles into preview iframe immediately
1973 injectPreviewStyles();
1974
1975 // Disable the AI Translator button
1976 toggleTranslatorButton(true);
1977
1978 // Get all translatable elements
1979 var elements = getTranslatableElements();
1980
1981 if (elements.length === 0) {
1982 alert('No translatable text elements found on this page.');
1983 translationState.isTranslating = false;
1984 toggleTranslatorButton(false);
1985 clearTranslationProgress();
1986 return;
1987 }
1988
1989 // Resuming: keep the elements already handled out of this run, but keep
1990 // counting them so the progress bar reflects the whole page.
1991 if (resumeFrom && Array.isArray(resumeFrom.done) && resumeFrom.done.length) {
1992 var alreadyDone = resumeFrom.done;
1993 var remaining = elements.filter(function(element) {
1994 return alreadyDone.indexOf(element.elementId) === -1;
1995 });
1996
1997 // Every element accounted for means there is nothing left to do.
1998 if (!remaining.length) {
1999 translationState.isTranslating = false;
2000 toggleTranslatorButton(false);
2001 clearTranslationProgress();
2002 alert('This page has already been translated.');
2003 return;
2004 }
2005
2006 translationState.doneElementIds = alreadyDone.slice();
2007 translationState.failedElementIds = Array.isArray(resumeFrom.failed) ? resumeFrom.failed.slice() : [];
2008 translationState.translatedElements = alreadyDone.length;
2009 translationState.failedElements = translationState.failedElementIds.length;
2010 translationState.resumedCount = alreadyDone.length;
2011 elements = remaining;
2012 } else {
2013 translationState.resumedCount = 0;
2014 }
2015
2016 translationState.totalElements = elements.length + translationState.doneElementIds.length;
2017 saveTranslationProgress();
2018
2019 // Update popup to show progress
2020 showProgressInPopup($popup);
2021
2022 // Animate popup to corner and start translation
2023 movePopupToCorner($popup, $overlay).then(function() {
2024 // Start translating elements one by one
2025 translateElementsSequentially(elements, 0, $popup);
2026 });
2027 }
2028
2029 /**
2030 * Get all translatable text elements
2031 */
2032 function getTranslatableElements() {
2033 var elements = [];
2034 // Get the main document container using Elementor 3.0+ API
2035 var documentContainer = elementor.documents.getCurrent().container;
2036 var elementorElements = [];
2037
2038 // Use the new API to get children - for Elementor 3.0+
2039 if (documentContainer.children && typeof documentContainer.children.models !== 'undefined') {
2040 // Backbone collection - extract models
2041 elementorElements = documentContainer.children.models || [];
2042 } else if (documentContainer.elements && typeof documentContainer.elements.models !== 'undefined') {
2043 // Alternative property name in some Elementor versions
2044 elementorElements = documentContainer.elements.models || [];
2045 } else if (Array.isArray(documentContainer.children)) {
2046 // Fallback for older API
2047 elementorElements = documentContainer.children;
2048 } else {
2049 // console.warn('🚨 Unable to find container children using any known API');
2050 elementorElements = [];
2051 }
2052
2053 function processContainer(container) {
2054 var model = container.model;
2055 var elementType = model.get('elType');
2056 var widgetType = model.get('widgetType');
2057
2058 // Process text-based widgets
2059 if (widgetType) {
2060 // First, check if this widget type should be skipped entirely
2061 var nonTextWidgets = [
2062 'spacer', 'divider', 'html', 'shortcode', 'sidebar',
2063 'menu-anchor', 'read-more', 'google_maps', 'paypal_button',
2064 'stripe_button', 'facebook_button', 'facebook_page',
2065 'video', 'audio', 'iframe', 'code', 'wp-widget',
2066 'map', 'rating', 'progress', 'counter', 'countdown',
2067 'social-icons', 'share-buttons', 'login', 'lottie',
2068 'image' // Image widget should be skipped
2069 ];
2070
2071 if (nonTextWidgets.indexOf(widgetType) !== -1) {
2072 return; // Exit early for blacklisted widgets
2073 }
2074
2075 var settings = model.get('settings').attributes;
2076
2077 // Now check for text fields in remaining widgets
2078 var textFields = getTextFieldsForWidget(widgetType, settings, container);
2079
2080 // If we found text fields, process the widget
2081 if (textFields.length > 0) {
2082 elements.push({
2083 container: container,
2084 widgetType: widgetType,
2085 textFields: textFields,
2086 elementId: model.get('id')
2087 });
2088 return;
2089 }
2090 }
2091
2092 // Process child containers recursively using Elementor 3.0+ API
2093 if (container.children && container.children.length > 0) {
2094 // Check if children is a Backbone collection
2095 if (typeof container.children.models !== 'undefined') {
2096 container.children.models.forEach(processContainer);
2097 } else if (Array.isArray(container.children)) {
2098 container.children.forEach(processContainer);
2099 }
2100 }
2101 }
2102
2103 elementorElements.forEach(processContainer);
2104 return elements;
2105 }
2106
2107 /**
2108 * Get text fields for a specific widget type using Elementor control types
2109 */
2110 function getTextFieldsForWidget(widgetType, settings, container) {
2111 var textFields = [];
2112
2113 // Try to get widget controls schema from Elementor
2114 var controls = getWidgetControls(widgetType, container);
2115
2116 if (controls && Object.keys(controls).length > 0) {
2117 // Look for text-based controls
2118 Object.keys(controls).forEach(function(controlName) {
2119 var control = controls[controlName];
2120 var controlType = control.type;
2121 var settingValue = settings[controlName];
2122
2123 // Check if this is a text-based control type
2124 var textControlTypes = [
2125 'text', 'textarea', 'wysiwyg', 'url', 'email',
2126 'password', 'search', 'tel', 'date', 'time',
2127 'datetime-local', 'month', 'week'
2128 ];
2129
2130 if (textControlTypes.includes(controlType)) {
2131 // Check if the field has a non-empty string value
2132 if (settingValue && typeof settingValue === 'string' && settingValue.trim()) {
2133 // Skip obviously non-translatable fields
2134 var skipFields = [
2135 '_element_id', '_css_classes', 'link', 'url', 'href',
2136 'custom_css', 'css_id', 'anchor', 'html_tag'
2137 ];
2138
2139 if (!skipFields.includes(controlName)) {
2140 textFields.push({
2141 field: controlName,
2142 value: settingValue,
2143 type: controlType === 'wysiwyg' ? 'wysiwyg' : 'text'
2144 });
2145 }
2146 }
2147 }
2148
2149 // Also check for repeater controls
2150 if (controlType === 'repeater' && settingValue) {
2151 checkRepeaterFieldsByType(controlName, control, settingValue, textFields);
2152 }
2153 });
2154 } else {
2155 // Fallback: Use the original method for widgets without accessible controls
2156 var commonTextFields = [
2157 'title', 'text', 'content', 'description', 'subtitle', 'button_text',
2158 'heading_title', 'heading_subtitle', 'testimonial_content', 'testimonial_name',
2159 'title_text', 'description_text', 'content_text', 'editor'
2160 ];
2161
2162 commonTextFields.forEach(function(field) {
2163 if (settings[field] && typeof settings[field] === 'string' && settings[field].trim()) {
2164 textFields.push({
2165 field: field,
2166 value: settings[field],
2167 type: field === 'editor' ? 'wysiwyg' : 'text'
2168 });
2169 }
2170 });
2171
2172 // Check for repeater fields using the old method
2173 checkRepeaterFields(settings, textFields);
2174 }
2175
2176 return textFields;
2177 }
2178
2179 /**
2180 * Get widget controls schema from Elementor
2181 */
2182 function getWidgetControls(widgetType, container) {
2183 try {
2184 // Method 1: Try to get controls from container model
2185 if (container && container.model && container.model.get) {
2186 var model = container.model;
2187
2188 // Try to get controls from the model's widget config
2189 if (model.config && model.config.controls) {
2190 return model.config.controls;
2191 }
2192
2193 // Try to get controls from the container settings
2194 if (container.settings && container.settings.controls) {
2195 return container.settings.controls;
2196 }
2197 }
2198
2199 // Method 2: Try to get controls from Elementor widgets registry
2200 if (window.elementor && elementor.widgets) {
2201 var widgetConfig = elementor.widgets.getWidgetType(widgetType);
2202 if (widgetConfig && widgetConfig.controls) {
2203 return widgetConfig.controls;
2204 }
2205 }
2206
2207 // Method 3: Try to get controls from elements manager
2208 if (window.elementor && elementor.elementsManager) {
2209 var elementView = elementor.elementsManager.getElementView(container.model.get('id'));
2210 if (elementView && elementView.model && elementView.model.controls) {
2211 return elementView.model.controls;
2212 }
2213 }
2214
2215 return null;
2216
2217 } catch (error) {
2218 // console.warn('⚠️ Error getting widget controls:', error);
2219 return null;
2220 }
2221 }
2222
2223 /**
2224 * Check repeater fields using control type information
2225 */
2226 function checkRepeaterFieldsByType(repeaterName, repeaterControl, repeaterData, textFields) {
2227 try {
2228 // Get the fields schema for this repeater
2229 var repeaterFields = repeaterControl.fields || repeaterControl.controls || {};
2230
2231 // Find text-based fields in the repeater schema
2232 var textFieldNames = [];
2233 Object.keys(repeaterFields).forEach(function(fieldName) {
2234 var fieldControl = repeaterFields[fieldName];
2235 var textControlTypes = ['text', 'textarea', 'wysiwyg', 'url', 'email'];
2236
2237 if (textControlTypes.includes(fieldControl.type)) {
2238 textFieldNames.push(fieldName);
2239 }
2240 });
2241
2242 if (textFieldNames.length === 0) {
2243 return;
2244 }
2245
2246 // Process repeater data (same as before)
2247 if (repeaterData && typeof repeaterData === 'object' && repeaterData.models) {
2248 // Backbone collection
2249 const models = repeaterData.models || [];
2250 for (let i = 0; i < models.length; i++) {
2251 const model = models[i];
2252 const modelData = model.attributes || model.toJSON();
2253
2254 for (const fieldName of textFieldNames) {
2255 if (modelData[fieldName] && typeof modelData[fieldName] === 'string' && modelData[fieldName].trim()) {
2256 const fieldKey = `${repeaterName}[${i}][${fieldName}]`;
2257 const fieldValue = modelData[fieldName];
2258
2259 textFields.push({
2260 field: fieldKey,
2261 value: fieldValue,
2262 type: 'text',
2263 isRepeater: true,
2264 repeaterKey: repeaterName,
2265 repeaterIndex: i,
2266 repeaterField: fieldName
2267 });
2268 }
2269 }
2270 }
2271 } else if (Array.isArray(repeaterData)) {
2272 // Regular array
2273 for (let i = 0; i < repeaterData.length; i++) {
2274 const item = repeaterData[i];
2275 for (const fieldName of textFieldNames) {
2276 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
2277 const fieldKey = `${repeaterName}[${i}][${fieldName}]`;
2278 const fieldValue = item[fieldName];
2279
2280 textFields.push({
2281 field: fieldKey,
2282 value: fieldValue,
2283 type: 'text',
2284 isRepeater: true,
2285 repeaterKey: repeaterName,
2286 repeaterIndex: i,
2287 repeaterField: fieldName
2288 });
2289 }
2290 }
2291 }
2292 }
2293
2294 } catch (error) {
2295 // console.warn('⚠️ Error processing repeater by type:', error);
2296 // Fallback to old method
2297 var repeaterConfig = {};
2298 repeaterConfig[repeaterName] = ['content', 'text', 'title', 'description'];
2299 checkRepeaterFields({[repeaterName]: repeaterData}, textFields);
2300 }
2301 }
2302
2303 /**
2304 * Check for repeater fields in settings (fallback method)
2305 */
2306 function checkRepeaterFields(settings, textFields) {
2307 // King Addons specific repeater configurations
2308 const repeaterConfigs = {
2309 'kng_styled_txt_content_items': ['kng_styled_txt_content'],
2310 'kng_tabs_items': ['kng_tabs_title', 'kng_tabs_content'],
2311 'kng_accordion_items': ['kng_accordion_title', 'kng_accordion_content'],
2312 'kng_testimonials_items': ['kng_testimonials_content', 'kng_testimonials_name'],
2313 'kng_team_members': ['kng_team_name', 'kng_team_position', 'kng_team_description'],
2314 'kng_price_list_items': ['kng_price_title', 'kng_price_description'],
2315 'kng_business_hours_items': ['kng_business_day', 'kng_business_hours'],
2316 // Standard Elementor repeaters
2317 'tabs': ['tab_title', 'tab_content'],
2318 'icon_list': ['text'],
2319 'slides': ['heading', 'description', 'button_text'],
2320 'list_items': ['text'],
2321 'testimonials': ['testimonial_content', 'testimonial_name'],
2322 'items': ['item_title', 'item_description', 'item_content'],
2323 'price_list': ['price_title', 'price_description']
2324 };
2325
2326 for (const [repeaterKey, fieldNames] of Object.entries(repeaterConfigs)) {
2327 if (settings[repeaterKey]) {
2328 let repeaterData = settings[repeaterKey];
2329
2330 // Handle Backbone Collections (common in King Addons and some Elementor widgets)
2331 if (repeaterData && typeof repeaterData === 'object' && repeaterData.models) {
2332 // Extract models from Backbone collection
2333 const models = repeaterData.models || [];
2334 for (let i = 0; i < models.length; i++) {
2335 const model = models[i];
2336 const modelData = model.attributes || model.toJSON();
2337
2338 for (const fieldName of fieldNames) {
2339 if (modelData[fieldName] && typeof modelData[fieldName] === 'string' && modelData[fieldName].trim()) {
2340 const fieldKey = `${repeaterKey}[${i}][${fieldName}]`;
2341 const fieldValue = modelData[fieldName];
2342
2343 textFields.push({
2344 field: fieldKey,
2345 value: fieldValue,
2346 type: 'text',
2347 isRepeater: true,
2348 repeaterKey: repeaterKey,
2349 repeaterIndex: i,
2350 repeaterField: fieldName
2351 });
2352 }
2353 }
2354 }
2355 }
2356 // Handle regular arrays
2357 else if (Array.isArray(repeaterData)) {
2358 for (let i = 0; i < repeaterData.length; i++) {
2359 const item = repeaterData[i];
2360 for (const fieldName of fieldNames) {
2361 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
2362 const fieldKey = `${repeaterKey}[${i}][${fieldName}]`;
2363 const fieldValue = item[fieldName];
2364
2365 textFields.push({
2366 field: fieldKey,
2367 value: fieldValue,
2368 type: 'text',
2369 isRepeater: true,
2370 repeaterKey: repeaterKey,
2371 repeaterIndex: i,
2372 repeaterField: fieldName
2373 });
2374 }
2375 }
2376 }
2377 }
2378 // Handle objects with numbered keys (alternative format)
2379 else if (repeaterData && typeof repeaterData === 'object') {
2380 const keys = Object.keys(repeaterData).filter(key => /^\d+$/.test(key));
2381
2382 for (const key of keys) {
2383 const item = repeaterData[key];
2384 for (const fieldName of fieldNames) {
2385 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
2386 const fieldKey = `${repeaterKey}[${key}][${fieldName}]`;
2387 const fieldValue = item[fieldName];
2388
2389 textFields.push({
2390 field: fieldKey,
2391 value: fieldValue,
2392 type: 'text',
2393 isRepeater: true,
2394 repeaterKey: repeaterKey,
2395 repeaterIndex: key,
2396 repeaterField: fieldName
2397 });
2398 }
2399 }
2400 }
2401 }
2402 }
2403 }
2404 }
2405
2406 /**
2407 * Show progress UI in popup
2408 */
2409 function showProgressInPopup($popup) {
2410 // Update header for compact mode with close button
2411 var headerHtml = `
2412 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);"/>
2413 AI is working on your page
2414 <button class="king-addons-translator-close-btn" title="Close">×</button>
2415 `;
2416
2417 var progressHtml = `
2418 <div class="king-addons-translator-progress">
2419 <div class="king-addons-translator-progress-text">
2420 Processing page elements&hellip; <span id="king-addons-progress-count">0 / ${translationState.totalElements}</span>
2421 </div>
2422 <div class="king-addons-translator-progress-bar">
2423 <div class="king-addons-translator-progress-fill" id="king-addons-progress-fill"></div>
2424 </div>
2425 <div class="ka-tr-activity">
2426 <span class="ka-tr-spinner" aria-hidden="true"></span>
2427 <span class="king-addons-translator-current-element" id="king-addons-current-element">Preparing…</span>
2428 </div>
2429 <div class="ka-tr-snippet" id="king-addons-progress-snippet" hidden></div>
2430 <div class="king-addons-translator-progress-note" id="king-addons-progress-note"></div>
2431 </div>
2432 `;
2433
2434 // Update header
2435 $popup.find('h3').html(headerHtml);
2436
2437 // Hide the description and the byline while the run is in progress.
2438 $popup.find('.ka-tr-dialog-sub, .ka-tr-byline').hide();
2439
2440 $popup.find('.king-addons-translator-form').html(progressHtml);
2441
2442 // Note: Close button events are handled by global document handler to prevent duplicates
2443 }
2444
2445 /**
2446 * Translate elements sequentially
2447 */
2448 function translateElementsSequentially(elements, index, $popup) {
2449 // Check if translation was cancelled
2450 if (translationState.isCancelled) {
2451 return;
2452 }
2453
2454 if (index >= elements.length) {
2455 showTranslationComplete($popup);
2456 return;
2457 }
2458
2459 var element = elements[index];
2460 translationState.currentElement = element;
2461
2462 // Update progress UI
2463 updateProgressUI(translationState.doneElementIds.length + 1, element);
2464
2465 // Highlight current element in preview
2466 highlightElementInPreview(element.elementId, true);
2467
2468 // Translate all text fields for this element
2469 translateElementFields(element, function(success) {
2470 // Remove highlight
2471 highlightElementInPreview(element.elementId, false);
2472
2473 // A fatal error stops the run from inside the request handler; do
2474 // not record the element or schedule the next one.
2475 if (translationState.isCancelled) {
2476 return;
2477 }
2478
2479 if (success) {
2480 translationState.translatedElements++;
2481 translationState.consecutiveFailures = 0;
2482 showElementSuccess(element.elementId);
2483 } else {
2484 translationState.failedElements++;
2485 translationState.consecutiveFailures++;
2486 translationState.failedElementIds.push(element.elementId);
2487 }
2488
2489 // Either way the element is behind us, so a resume skips it.
2490 translationState.doneElementIds.push(element.elementId);
2491 saveTranslationProgress();
2492
2493 // A long run of failures means the provider or model is unusable;
2494 // stop rather than working through the rest of the page for nothing.
2495 if (translationState.consecutiveFailures >= MAX_CONSECUTIVE_ELEMENT_FAILURES) {
2496 handleFatalTranslationError({
2497 code: 'unknown',
2498 message: (translationState.lastErrorMessage || 'Several elements failed in a row.')
2499 + '\n\nTranslation stopped after '
2500 + MAX_CONSECUTIVE_ELEMENT_FAILURES
2501 + ' consecutive failures. You can resume it later from where it stopped.'
2502 });
2503 return;
2504 }
2505
2506 // Continue with next element after a short delay
2507 setTimeout(function() {
2508 // Check if translation was cancelled before proceeding
2509 if (!translationState.isCancelled) {
2510 translateElementsSequentially(elements, index + 1, $popup);
2511 }
2512 }, 500);
2513 });
2514 }
2515
2516 /**
2517 * Translate all text fields for an element
2518 */
2519 function translateElementFields(element, callback) {
2520 // Check if translation was cancelled before starting
2521 if (translationState.isCancelled) {
2522 callback(false);
2523 return;
2524 }
2525
2526 var fieldsToTranslate = element.textFields.slice();
2527 var translatedFields = {};
2528 var completedFields = 0;
2529 var hasErrors = false;
2530
2531 if (fieldsToTranslate.length === 0) {
2532 callback(true);
2533 return;
2534 }
2535
2536 function translateNextField() {
2537 // Check if translation was cancelled before processing next field
2538 if (translationState.isCancelled) {
2539 callback(false);
2540 return;
2541 }
2542
2543 if (completedFields >= fieldsToTranslate.length) {
2544 // All fields translated, update the element
2545 if (Object.keys(translatedFields).length > 0 && !translationState.isCancelled) {
2546 updateElementSettings(element.container, translatedFields);
2547 }
2548 callback(!hasErrors);
2549 return;
2550 }
2551
2552 var field = fieldsToTranslate[completedFields];
2553 setActivity(element, completedFields + 1, fieldsToTranslate.length, field.value);
2554 translateSingleField(field.value, function(translatedText, success) {
2555 // Check if translation was cancelled while waiting for response
2556 if (translationState.isCancelled) {
2557 callback(false);
2558 return;
2559 }
2560
2561 if (success && translatedText) {
2562 translatedFields[field.field] = translatedText;
2563 } else {
2564 hasErrors = true;
2565 }
2566
2567 completedFields++;
2568 setTimeout(translateNextField, 200); // Small delay between field translations
2569 });
2570 }
2571
2572 translateNextField();
2573 }
2574
2575 /**
2576 * Translate a single text field
2577 */
2578 // How many times a temporary failure is retried before giving up on a field,
2579 // and how long to wait before each retry.
2580 var RETRY_DELAYS = [2000, 5000, 12000];
2581
2582 // A model that is throttled or down fails every field, so the run stops
2583 // rather than grinding through the whole page collecting failures.
2584 var MAX_CONSECUTIVE_ELEMENT_FAILURES = 5;
2585
2586 /**
2587 * Normalise a failed translation request into { code, message, retryable }.
2588 *
2589 * The server classifies provider failures and answers with a meaningful
2590 * status, but requests can also fail before reaching it (offline, proxy,
2591 * PHP fatal), so the status code is used as a fallback.
2592 */
2593 function parseTranslationError(response, xhr) {
2594 var data = null;
2595
2596 if (response && typeof response === 'object' && response.data) {
2597 data = response.data;
2598 } else if (xhr && xhr.responseJSON && xhr.responseJSON.data) {
2599 data = xhr.responseJSON.data;
2600 } else if (xhr && xhr.responseText) {
2601 try {
2602 var parsed = JSON.parse(xhr.responseText);
2603 data = parsed && parsed.data;
2604 } catch (e) {
2605 // Not JSON - fall back to the status code below.
2606 }
2607 }
2608
2609 // No xhr means the HTTP call itself succeeded and the body carried the
2610 // failure, so it must not be mistaken for a lost connection.
2611 var status = xhr && typeof xhr.status === 'number' ? xhr.status : (response ? 200 : 0);
2612 var message = '';
2613 var code = '';
2614 var retryable = null;
2615
2616 if (data && typeof data === 'object') {
2617 message = data.message || '';
2618 code = data.code || '';
2619 if (typeof data.retryable === 'boolean') {
2620 retryable = data.retryable;
2621 }
2622 } else if (typeof data === 'string') {
2623 message = data;
2624 }
2625
2626 if (!code) {
2627 if (status === 200) {
2628 code = 'unknown';
2629 } else if (status === 0) {
2630 code = 'network';
2631 } else if (status === 401 || status === 403) {
2632 code = 'auth';
2633 } else if (status === 402) {
2634 code = 'credits';
2635 } else if (status === 400 || status === 404) {
2636 code = 'model';
2637 } else if (status === 429) {
2638 code = 'rate_limit';
2639 } else if (status >= 500) {
2640 code = 'upstream';
2641 } else {
2642 code = 'unknown';
2643 }
2644 }
2645
2646 if (retryable === null) {
2647 retryable = (code === 'rate_limit' || code === 'upstream' || code === 'network');
2648 }
2649
2650 if (!message) {
2651 var fallbacks = {
2652 network: 'Network connection failed. Please check your internet connection.',
2653 auth: 'The API key is invalid or expired. Please check it in AI Settings.',
2654 credits: 'The AI provider reports insufficient credits.',
2655 model: 'The selected model was rejected by the provider. Pick another model in AI Settings.',
2656 rate_limit: 'Rate limit reached. Please wait a moment and try again.',
2657 daily_limit: 'The daily limit for this model has been reached.',
2658 upstream: 'The AI provider is temporarily unavailable. Please try again shortly.'
2659 };
2660 message = fallbacks[code] || 'Translation failed.';
2661 }
2662
2663 return { code: code, message: message, retryable: retryable, status: status };
2664 }
2665
2666 /**
2667 * Stop the run and explain why, choosing the popup that fits the cause.
2668 */
2669 function handleFatalTranslationError(error) {
2670 // Keep whatever has been translated so far resumable.
2671 saveTranslationProgress();
2672 stopTranslationProcess();
2673
2674 var providerLabel = (window.KingAddonsAiField && KingAddonsAiField.provider_label) || 'AI provider';
2675
2676 setTimeout(function() {
2677 if (error.code === 'auth') {
2678 showApiKeyError('Setup Required', error.message + '\n\nPlease check your API key in AI Settings and try again.');
2679 } else if (error.code === 'credits' || error.code === 'daily_limit'
2680 || error.code === 'rate_limit' || error.code === 'local_limit') {
2681 showTokenLimitError(error.message, error.code);
2682 } else if (error.code === 'model') {
2683 showApiKeyError('Model Not Available', error.message);
2684 } else {
2685 showApiKeyError('Run Stopped', error.message);
2686 }
2687 }, 500);
2688 }
2689
2690 /**
2691 * Show a short-lived note in the progress popup (retry countdown, warnings).
2692 */
2693 function setProgressNote(text) {
2694 var $note = $('#king-addons-progress-note');
2695 if (!$note.length) {
2696 return;
2697 }
2698 if (text) {
2699 $note.text(text).show();
2700 } else {
2701 $note.text('').hide();
2702 }
2703 }
2704
2705 /**
2706 * Translate a single text field, retrying temporary provider failures.
2707 */
2708 function translateSingleField(text, callback, attempt) {
2709 attempt = attempt || 0;
2710
2711 // Check if translation was cancelled before making request
2712 if (translationState.isCancelled) {
2713 callback(text, false);
2714 return;
2715 }
2716
2717 var request = $.post(KingAddonsAiField.ajax_url, {
2718 action: 'king_addons_ai_translate_text',
2719 nonce: KingAddonsAiField.generate_nonce,
2720 text: text,
2721 from_lang: translationState.fromLang,
2722 to_lang: translationState.toLang
2723 });
2724
2725 // Store the request so we can cancel it if needed
2726 translationState.currentRequests.push(request);
2727
2728 function releaseRequest() {
2729 var index = translationState.currentRequests.indexOf(request);
2730 if (index > -1) {
2731 translationState.currentRequests.splice(index, 1);
2732 }
2733 }
2734
2735 function onFailure(error) {
2736 if (translationState.isCancelled) {
2737 callback(text, false);
2738 return;
2739 }
2740
2741 // Temporary problem: wait and try the same field again.
2742 if (error.retryable && attempt < RETRY_DELAYS.length) {
2743 var delay = RETRY_DELAYS[attempt];
2744 setProgressNote('⏳ ' + error.message + ' Retrying in ' + Math.round(delay / 1000) + 's…');
2745
2746 setTimeout(function() {
2747 if (translationState.isCancelled) {
2748 callback(text, false);
2749 return;
2750 }
2751 setProgressNote('');
2752 translateSingleField(text, callback, attempt + 1);
2753 }, delay);
2754 return;
2755 }
2756
2757 setProgressNote('');
2758
2759 // A dead end (bad key, no credit, unusable model, daily cap) will
2760 // fail every remaining field, so stop instead of burning the page.
2761 var fatalCodes = ['auth', 'credits', 'model', 'daily_limit', 'rate_limit', 'local_limit'];
2762 if (fatalCodes.indexOf(error.code) > -1) {
2763 handleFatalTranslationError(error);
2764 return;
2765 }
2766
2767 // Anything else: give up on this field and let the run continue.
2768 translationState.lastErrorMessage = error.message;
2769 callback(text, false);
2770 }
2771
2772 request.done(function(response) {
2773 releaseRequest();
2774
2775 if (translationState.isCancelled) {
2776 callback(text, false);
2777 return;
2778 }
2779
2780 if (response && response.success && response.data && response.data.translated_text) {
2781 setProgressNote('');
2782 callback(response.data.translated_text, true);
2783 return;
2784 }
2785
2786 onFailure(parseTranslationError(response, null));
2787 }).fail(function(xhr, textStatus) {
2788 releaseRequest();
2789
2790 // An aborted request is a cancellation, not a provider failure.
2791 if (translationState.isCancelled || textStatus === 'abort') {
2792 return;
2793 }
2794
2795 onFailure(parseTranslationError(null, xhr));
2796 });
2797 }
2798
2799 /**
2800 * Update element settings with translated text
2801 */
2802 function updateElementSettings(container, translatedFields) {
2803 try {
2804 // Separate regular fields from repeater fields
2805 var regularFields = {};
2806 var repeaterUpdates = {};
2807
2808 Object.keys(translatedFields).forEach(function(fieldKey) {
2809 var translatedValue = translatedFields[fieldKey];
2810
2811 // Check if this is a repeater field
2812 var repeaterMatch = fieldKey.match(/^(.+)\[(\d+)\]\[(.+)\]$/);
2813 if (repeaterMatch) {
2814 // This is a repeater field: repeaterKey[index][fieldName]
2815 var repeaterKey = repeaterMatch[1];
2816 var itemIndex = parseInt(repeaterMatch[2]);
2817 var itemField = repeaterMatch[3];
2818
2819 if (!repeaterUpdates[repeaterKey]) {
2820 repeaterUpdates[repeaterKey] = {};
2821 }
2822 if (!repeaterUpdates[repeaterKey][itemIndex]) {
2823 repeaterUpdates[repeaterKey][itemIndex] = {};
2824 }
2825 repeaterUpdates[repeaterKey][itemIndex][itemField] = translatedValue;
2826 } else {
2827 // Regular field
2828 regularFields[fieldKey] = translatedValue;
2829 }
2830 });
2831
2832 // Apply regular field updates
2833 if (Object.keys(regularFields).length > 0) {
2834 $e.run('document/elements/settings', {
2835 container: container,
2836 settings: regularFields
2837 });
2838 }
2839
2840 // Apply repeater field updates
2841 Object.keys(repeaterUpdates).forEach(function(repeaterKey) {
2842 var currentSettings = container.settings.get(repeaterKey);
2843
2844 // Handle Backbone Collections (King Addons and some Elementor widgets)
2845 if (currentSettings && typeof currentSettings.models !== 'undefined') {
2846 // Work with Backbone collection
2847 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2848 var index = parseInt(itemIndex);
2849 if (currentSettings.models[index]) {
2850 var model = currentSettings.models[index];
2851
2852 // Update the specific fields in this repeater item
2853 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2854 var oldValue = model.get(fieldName);
2855 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2856
2857 // Update the model attribute
2858 model.set(fieldName, newValue);
2859 });
2860 }
2861 });
2862
2863 // Use Elementor's proper API to notify of changes instead of direct trigger
2864 try {
2865 // Method 1: Use Elementor's run command to update the entire repeater
2866 var backboneData = currentSettings.toJSON ? currentSettings.toJSON() :
2867 currentSettings.models.map(function(model) {
2868 return model.toJSON ? model.toJSON() : model.attributes;
2869 });
2870
2871 var repeaterSettings = {};
2872 repeaterSettings[repeaterKey] = backboneData;
2873
2874 $e.run('document/elements/settings', {
2875 container: container,
2876 settings: repeaterSettings
2877 });
2878 } catch (e) {
2879 // console.warn('⚠️ Error updating via Elementor API, trying alternative method:', e);
2880
2881 // Fallback: Try to manually trigger save without change events
2882 try {
2883 if (typeof container.saveSettings === 'function') {
2884 container.saveSettings();
2885 }
2886 } catch (e2) {
2887 // console.warn('⚠️ Fallback method also failed:', e2);
2888 }
2889 }
2890 }
2891 // Handle regular arrays (standard Elementor repeaters)
2892 else if (Array.isArray(currentSettings)) {
2893 var updatedRepeater = currentSettings.slice(); // Clone array
2894
2895 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2896 var index = parseInt(itemIndex);
2897 if (updatedRepeater[index]) {
2898 // Update the specific fields in this repeater item
2899 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2900 var oldValue = updatedRepeater[index][fieldName];
2901 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2902 updatedRepeater[index][fieldName] = newValue;
2903 });
2904 }
2905 });
2906
2907 // Update the entire repeater field
2908 var repeaterSettings = {};
2909 repeaterSettings[repeaterKey] = updatedRepeater;
2910
2911 $e.run('document/elements/settings', {
2912 container: container,
2913 settings: repeaterSettings
2914 });
2915 }
2916 // Handle objects with numbered keys
2917 else if (currentSettings && typeof currentSettings === 'object') {
2918 var updatedObject = Object.assign({}, currentSettings); // Clone object
2919
2920 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2921 if (updatedObject[itemIndex]) {
2922 // Update the specific fields in this repeater item
2923 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2924 var oldValue = updatedObject[itemIndex][fieldName];
2925 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2926 updatedObject[itemIndex][fieldName] = newValue;
2927 });
2928 }
2929 });
2930
2931 // Update the entire repeater field
2932 var repeaterSettings = {};
2933 repeaterSettings[repeaterKey] = updatedObject;
2934
2935 $e.run('document/elements/settings', {
2936 container: container,
2937 settings: repeaterSettings
2938 });
2939 } else {
2940 }
2941 });
2942
2943 } catch (error) {
2944 // console.error('❌ Error updating element settings:', error);
2945 // console.error('Error details:', {
2946 // message: error.message,
2947 // stack: error.stack,
2948 // translatedFields: translatedFields,
2949 // widgetType: container.model.get('widgetType')
2950 // });
2951 }
2952 }
2953
2954 /**
2955 * Update progress UI
2956 */
2957 function updateProgressUI(current, element) {
2958 var percentage = (current / translationState.totalElements) * 100;
2959
2960 $('#king-addons-progress-count').text(current + ' / ' + translationState.totalElements);
2961 $('#king-addons-progress-fill').css('width', percentage + '%');
2962 setActivity(element, 0, element.textFields.length, '');
2963 }
2964
2965 /**
2966 * Describe what the translator is working on right now.
2967 *
2968 * @param {Object} element Element being processed.
2969 * @param {number} fieldIndex 1-based field position, 0 while starting out.
2970 * @param {number} fieldTotal Number of fields on the element.
2971 * @param {string} text Source text of the current field.
2972 */
2973 function setActivity(element, fieldIndex, fieldTotal, text) {
2974 var label = element ? element.widgetType : '';
2975 if (fieldTotal > 1 && fieldIndex > 0) {
2976 label += ' — field ' + fieldIndex + ' of ' + fieldTotal;
2977 }
2978 if (translationState.toLang) {
2979 label += ' → ' + describeLanguage(translationState.toLang);
2980 }
2981
2982 $('#king-addons-current-element').text(label);
2983
2984 // Showing the actual string makes a long run legible: you can see it
2985 // move rather than watching a counter that only ticks per element.
2986 var $snippet = $('#king-addons-progress-snippet');
2987 var plain = $('<div></div>').html(String(text || '')).text().replace(/\s+/g, ' ').trim();
2988 if (plain) {
2989 $snippet.text(plain.length > 160 ? plain.slice(0, 160) + '…' : plain).prop('hidden', false);
2990 } else {
2991 $snippet.text('').prop('hidden', true);
2992 }
2993 }
2994
2995 /**
2996 * Inject animation styles into preview iframe
2997 */
2998 function injectPreviewStyles() {
2999 if (!elementor || !elementor.$preview) return;
3000
3001 var $previewDoc = elementor.$preview.contents();
3002 var $previewHead = $previewDoc.find('head');
3003
3004 if ($previewHead.length && !$previewDoc.find('#king-addons-preview-translator-styles').length) {
3005 var previewStyles = `
3006 <style id="king-addons-preview-translator-styles">
3007 /* Element highlighting animation for translation */
3008 .king-addons-translating-element {
3009 position: relative !important;
3010 border: 3px solid #2196F3 !important;
3011 box-shadow: 0 0 20px rgba(33, 150, 243, 0.4) !important;
3012 border-radius: 4px !important;
3013 animation: king-addons-translate-pulse 1.5s infinite ease-in-out !important;
3014 z-index: 999 !important;
3015 }
3016
3017 .king-addons-translating-element::before {
3018 content: "🔄 Translating..." !important;
3019 position: absolute !important;
3020 top: -35px !important;
3021 left: 50% !important;
3022 transform: translateX(-50%) !important;
3023 background: #2196F3 !important;
3024 color: white !important;
3025 padding: 6px 12px !important;
3026 border-radius: 20px !important;
3027 font-size: 12px !important;
3028 font-weight: 600 !important;
3029 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
3030 z-index: 10000 !important;
3031 animation: king-addons-translate-bounce 0.8s ease-out !important;
3032 box-shadow: 0 3px 10px rgba(33, 150, 243, 0.3) !important;
3033 white-space: nowrap !important;
3034 }
3035
3036 .king-addons-translated-element {
3037 position: relative !important;
3038 border: 3px solid #4CAF50 !important;
3039 box-shadow: 0 0 20px rgba(76, 175, 80, 0.4) !important;
3040 border-radius: 4px !important;
3041 animation: king-addons-translate-success 1.2s ease-out !important;
3042 z-index: 999 !important;
3043 }
3044
3045 .king-addons-translated-element::before {
3046 content: "�
3047 Translated!" !important;
3048 position: absolute !important;
3049 top: -35px !important;
3050 left: 50% !important;
3051 transform: translateX(-50%) !important;
3052 background: #4CAF50 !important;
3053 color: white !important;
3054 padding: 6px 12px !important;
3055 border-radius: 20px !important;
3056 font-size: 12px !important;
3057 font-weight: 600 !important;
3058 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
3059 z-index: 10000 !important;
3060 animation: king-addons-translate-bounce 0.8s ease-out !important;
3061 box-shadow: 0 3px 10px rgba(76, 175, 80, 0.3) !important;
3062 white-space: nowrap !important;
3063 }
3064
3065 /* Pulsing animation for translating elements */
3066 @keyframes king-addons-translate-pulse {
3067 0% {
3068 box-shadow: 0 0 0 0 rgba(33, 150, 243, 0.7),
3069 0 0 20px rgba(33, 150, 243, 0.4);
3070 transform: scale(1);
3071 }
3072 50% {
3073 box-shadow: 0 0 0 8px rgba(33, 150, 243, 0.2),
3074 0 0 30px rgba(33, 150, 243, 0.6);
3075 transform: scale(1.02);
3076 }
3077 100% {
3078 box-shadow: 0 0 0 0 rgba(33, 150, 243, 0),
3079 0 0 20px rgba(33, 150, 243, 0.4);
3080 transform: scale(1);
3081 }
3082 }
3083
3084 /* Success animation for completed elements */
3085 @keyframes king-addons-translate-success {
3086 0% {
3087 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7),
3088 0 0 20px rgba(76, 175, 80, 0.4);
3089 transform: scale(1);
3090 }
3091 20% {
3092 box-shadow: 0 0 0 12px rgba(76, 175, 80, 0.3),
3093 0 0 40px rgba(76, 175, 80, 0.6);
3094 transform: scale(1.05);
3095 }
3096 40% {
3097 transform: scale(0.98);
3098 }
3099 60% {
3100 transform: scale(1.02);
3101 }
3102 80% {
3103 transform: scale(0.99);
3104 }
3105 100% {
3106 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0),
3107 0 0 20px rgba(76, 175, 80, 0.2);
3108 transform: scale(1);
3109 }
3110 }
3111
3112 /* Bounce animation for labels */
3113 @keyframes king-addons-translate-bounce {
3114 0% {
3115 transform: translateX(-50%) translateY(-10px) scale(0.8);
3116 opacity: 0;
3117 }
3118 50% {
3119 transform: translateX(-50%) translateY(-2px) scale(1.1);
3120 opacity: 1;
3121 }
3122 70% {
3123 transform: translateX(-50%) translateY(-1px) scale(0.95);
3124 }
3125 100% {
3126 transform: translateX(-50%) translateY(0) scale(1);
3127 opacity: 1;
3128 }
3129 }
3130 </style>
3131 `;
3132 $previewHead.append(previewStyles);
3133 }
3134 }
3135
3136 /**
3137 * Highlight element in preview
3138 */
3139 function highlightElementInPreview(elementId, highlight) {
3140 // Ensure preview styles are injected
3141 injectPreviewStyles();
3142
3143 // Find element in preview iframe
3144 if (!elementor || !elementor.$preview) {
3145 return;
3146 }
3147
3148 var $previewDoc = elementor.$preview.contents();
3149 var $previewElement = $previewDoc.find('[data-id="' + elementId + '"]');
3150
3151 if ($previewElement.length === 0) {
3152 return;
3153 }
3154
3155 if (highlight) {
3156 // Remove any existing classes first
3157 $previewElement.removeClass('king-addons-translated-element');
3158 $previewElement.addClass('king-addons-translating-element');
3159 scrollPreviewToElement($previewElement);
3160 } else {
3161 $previewElement.removeClass('king-addons-translating-element');
3162 }
3163 }
3164
3165 /**
3166 * Bring the element being translated into view inside the preview.
3167 *
3168 * A long page otherwise translates itself off screen, so the highlight and
3169 * the success animation are never actually seen.
3170 *
3171 * The preview iframe is not scrolled by plain window.scrollTo - Elementor
3172 * drives it itself - so its own helper is used, the same one the Navigator
3173 * uses to jump to a widget. It already skips elements that are in view and
3174 * animates the rest.
3175 *
3176 * @param {jQuery} $element Element inside the preview document.
3177 */
3178 function scrollPreviewToElement($element) {
3179 if (!$element || !$element.length) {
3180 return;
3181 }
3182
3183 try {
3184 if (elementor.helpers && typeof elementor.helpers.scrollToView === 'function') {
3185 // Second argument is the delay before scrolling; the default
3186 // half second would lag behind a fast run.
3187 elementor.helpers.scrollToView($element, 0);
3188 return;
3189 }
3190 } catch (e) {
3191 // Fall through to the native path below.
3192 }
3193
3194 try {
3195 $element[0].scrollIntoView({
3196 behavior: prefersReducedMotion() ? 'auto' : 'smooth',
3197 block: 'center'
3198 });
3199 } catch (e) {
3200 // A torn-down preview must not break the run.
3201 }
3202 }
3203
3204 /**
3205 * Whether the viewer asked for less animation.
3206 */
3207 function prefersReducedMotion() {
3208 try {
3209 return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
3210 } catch (e) {
3211 return false;
3212 }
3213 }
3214
3215 /**
3216 * Show element success animation
3217 */
3218 function showElementSuccess(elementId) {
3219 // Ensure preview styles are injected
3220 injectPreviewStyles();
3221
3222 // Find element in preview iframe
3223 if (!elementor || !elementor.$preview) {
3224 return;
3225 }
3226
3227 var $previewDoc = elementor.$preview.contents();
3228 var $previewElement = $previewDoc.find('[data-id="' + elementId + '"]');
3229
3230 if ($previewElement.length === 0) {
3231 return;
3232 }
3233
3234 // Remove translating class and add translated class
3235 $previewElement.removeClass('king-addons-translating-element');
3236 $previewElement.addClass('king-addons-translated-element');
3237
3238 // Remove the success animation after it completes
3239 setTimeout(function() {
3240 $previewElement.removeClass('king-addons-translated-element');
3241 }, 1200);
3242 }
3243
3244 /**
3245 * Show translation complete with stats (stays open until manually closed)
3246 */
3247 function showTranslationComplete($popup) {
3248 translationState.isTranslating = false;
3249
3250 var failedIds = translationState.failedElementIds.slice();
3251
3252 // Elements that failed are worth another attempt - a model can refuse
3253 // one string and handle it fine on a retry. Keeping a progress entry
3254 // that marks everything except the failures as done turns the normal
3255 // Resume path into "retry just the ones that failed".
3256 if (failedIds.length) {
3257 translationState.doneElementIds = translationState.doneElementIds.filter(function(id) {
3258 return failedIds.indexOf(id) === -1;
3259 });
3260 saveTranslationProgress();
3261 } else {
3262 // The page is done, so there is nothing left to resume.
3263 clearTranslationProgress();
3264 }
3265
3266 if (!$popup || $popup.length === 0) {
3267 // console.error('❌ Cannot show translation results: popup not found');
3268 return;
3269 }
3270
3271 var statsHtml = `
3272 <div class="king-addons-translator-progress">
3273 <div class="king-addons-translator-stats">
3274 <div class="king-addons-translator-stat">
3275 <div class="king-addons-translator-stat-number">${translationState.totalElements}</div>
3276 <div class="king-addons-translator-stat-label">Elements</div>
3277 </div>
3278 <div class="king-addons-translator-stat">
3279 <div class="king-addons-translator-stat-number" style="color: var(--ka-tr-success);">${translationState.translatedElements}</div>
3280 <div class="king-addons-translator-stat-label">Translated</div>
3281 </div>
3282 <div class="king-addons-translator-stat">
3283 <div class="king-addons-translator-stat-number" style="color: ${translationState.failedElements > 0 ? 'var(--ka-tr-danger)' : 'var(--ka-tr-ink-muted)'};">${translationState.failedElements}</div>
3284 <div class="king-addons-translator-stat-label">Failed</div>
3285 </div>
3286 </div>
3287 ${failedIds.length ? `
3288 <div class="ka-tr-panel ka-tr-panel--warning" style="margin-top: 16px;">
3289 <p>${failedIds.length} element${failedIds.length === 1 ? '' : 's'} could not be translated${
3290 translationState.lastErrorMessage
3291 ? ': ' + $('<div></div>').text(translationState.lastErrorMessage).html()
3292 : '.'
3293 }</p>
3294 </div>` : ''}
3295 <div class="king-addons-translator-actions" style="margin-top: 20px;">
3296 <button class="king-addons-translator-btn-secondary" id="king-addons-close-stats">Close</button>
3297 ${failedIds.length ? '<button class="king-addons-translator-btn-primary" id="king-addons-retry-failed">Retry failed</button>' : ''}
3298 </div>
3299 </div>
3300 `;
3301
3302 var $form = $popup.find('.king-addons-translator-form');
3303
3304 $form.html(statsHtml);
3305
3306 if (!failedIds.length) {
3307 $form.find('#king-addons-close-stats')
3308 .removeClass('king-addons-translator-btn-secondary')
3309 .addClass('king-addons-translator-btn-primary');
3310 }
3311
3312 $form.find('#king-addons-retry-failed').on('click', function() {
3313 var saved = loadTranslationProgress();
3314 $popup.closest('.king-addons-translator-overlay').remove();
3315 $popup.remove();
3316 if (saved) {
3317 showResumePopup(saved);
3318 }
3319 });
3320
3321 // Play success sound (Web Audio API)
3322 try {
3323 var audioContext = new (window.AudioContext || window.webkitAudioContext)();
3324 var oscillator = audioContext.createOscillator();
3325 var gainNode = audioContext.createGain();
3326
3327 oscillator.connect(gainNode);
3328 gainNode.connect(audioContext.destination);
3329
3330 oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
3331 oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1);
3332 oscillator.frequency.setValueAtTime(1200, audioContext.currentTime + 0.2);
3333
3334 gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
3335 gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
3336
3337 oscillator.start(audioContext.currentTime);
3338 oscillator.stop(audioContext.currentTime + 0.3);
3339 } catch (e) {
3340 }
3341
3342 // Show temporary notification to attract attention
3343 var $notificationBanner = $('<div style="position: fixed; top: 0; left: 0; right: 0; background: #10794a; color: #fff; padding: 12px; text-align: center; font-size: 14px; font-weight: 600; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; z-index: 1000000; animation: slideDown 0.4s ease;">Your page is ready &mdash; see the results panel.</div>');
3344 $('body').append($notificationBanner);
3345
3346 // Remove notification after 5 seconds
3347 setTimeout(function() {
3348 $notificationBanner.fadeOut(300, function() {
3349 $notificationBanner.remove();
3350 });
3351 }, 5000);
3352
3353 // Update header to show completion
3354 var completionHeaderHtml = `
3355 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);"/>
3356 Your page is ready
3357 `;
3358 $popup.find('h3').html(completionHeaderHtml);
3359
3360 // Restore the byline, and rewrite the description for the result - the
3361 // popup may have started life as the resume prompt, whose subtitle no
3362 // longer applies.
3363 $popup.find('.ka-tr-byline').show();
3364 $popup.find('.ka-tr-dialog-sub')
3365 .text('Into ' + describeLanguage(translationState.toLang) + '.')
3366 .show();
3367
3368 // If popup is in compact mode, move it back to center for better visibility
3369 if ($popup.hasClass('compact')) {
3370 // Remove compact class and positioning
3371 $popup.removeClass('compact moving');
3372 $popup.css({
3373 'position': 'fixed',
3374 'top': '50%',
3375 'left': '50%',
3376 'transform': 'translate(-50%, -50%)',
3377 'right': 'auto',
3378 'bottom': 'auto',
3379 'width': '500px',
3380 'max-width': '90vw',
3381 'z-index': '999999',
3382 'background': 'white',
3383 'border-radius': '8px',
3384 'box-shadow': '0 10px 25px rgba(0,0,0,0.2)',
3385 'opacity': '1',
3386 'visibility': 'visible'
3387 });
3388
3389 // Re-add overlay if it doesn't exist
3390 if (!$popup.closest('.king-addons-translator-overlay').length) {
3391 var $overlay = $('<div class="king-addons-translator-overlay"></div>').css({
3392 'position': 'fixed',
3393 'top': '0',
3394 'left': '0',
3395 'width': '100%',
3396 'height': '100%',
3397 'background': 'rgba(0, 0, 0, 0.5)',
3398 'z-index': '999998',
3399 'display': 'flex',
3400 'align-items': 'center',
3401 'justify-content': 'center'
3402 });
3403 $popup.wrap($overlay);
3404 } else {
3405 // Make sure existing overlay is visible
3406 $popup.closest('.king-addons-translator-overlay').css({
3407 'z-index': '999998',
3408 'display': 'flex'
3409 });
3410 }
3411
3412 // Add entrance animation
3413 $popup.css('opacity', '0').animate({'opacity': '1'}, 300);
3414 } else {
3415 // For non-compact popups, ensure they're also properly visible
3416 $popup.css({
3417 'z-index': '999999',
3418 'opacity': '1',
3419 'visibility': 'visible',
3420 'position': 'fixed'
3421 });
3422
3423 // Make sure overlay is visible
3424 var $overlay = $popup.closest('.king-addons-translator-overlay');
3425 if ($overlay.length) {
3426 $overlay.css({
3427 'z-index': '999998',
3428 'display': 'block',
3429 'opacity': '1',
3430 'visibility': 'visible'
3431 });
3432 }
3433
3434 // Add entrance animation
3435 $popup.css('opacity', '0').animate({'opacity': '1'}, 300);
3436 }
3437
3438 $('#king-addons-close-stats').on('click', function() {
3439 // For compact popup, just remove it directly since overlay is already gone
3440 if ($popup.hasClass('compact')) {
3441 $popup.remove();
3442 } else {
3443 $popup.closest('.king-addons-translator-overlay').remove();
3444 }
3445 });
3446
3447 // Reset translation state and re-enable button
3448 translationState.isTranslating = false;
3449 translationState.isCancelled = false;
3450 translationState.currentRequests = []; // Clear any remaining requests
3451 toggleTranslatorButton(false);
3452
3453 // Note: Auto-close removed by user request - popup stays open until manually closed
3454 }
3455
3456 /**
3457 * Handle Elementor initialization
3458 */
3459 function onElementorInit() {
3460 // Check if AI Page Translator is enabled
3461 if (typeof KingAddonsAiField !== 'undefined' && KingAddonsAiField.translator_enabled === false) {
3462 return;
3463 }
3464
3465 // Inject styles first
3466 injectTranslatorStyles();
3467
3468 // Try to inject preview styles (will work when preview is available)
3469 setTimeout(function() {
3470 injectPreviewStyles();
3471 }, 1000);
3472
3473 // Add button immediately
3474 addTranslatorButton();
3475
3476 // Also add button when panel opens
3477 if (typeof elementor !== 'undefined' && elementor.hooks) {
3478 elementor.hooks.addAction('panel/open_editor/widget', function() {
3479 setTimeout(addTranslatorButton, 100);
3480 });
3481
3482 // Add button when navigator opens
3483 elementor.hooks.addAction('navigator/init', function() {
3484 setTimeout(addTranslatorButton, 100);
3485 });
3486
3487 // Inject preview styles when preview loads
3488 elementor.hooks.addAction('preview/loaded', function() {
3489 injectPreviewStyles();
3490 });
3491 }
3492
3493 // Monitor for panel changes
3494 observePanelChanges();
3495
3496 // Surface an interrupted run once the document is available.
3497 setTimeout(offerResumeOnLoad, 1500);
3498 }
3499
3500 /**
3501 * Observe panel changes to re-add button if needed
3502 */
3503 function observePanelChanges() {
3504 function createObserver() {
3505 return new MutationObserver(function(mutations) {
3506 var shouldCheck = false;
3507
3508 mutations.forEach(function(mutation) {
3509 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
3510 shouldCheck = true;
3511 }
3512 });
3513
3514 if (shouldCheck) {
3515 setTimeout(addTranslatorButton, 300);
3516 }
3517 });
3518 }
3519
3520 // Observe changes in the top toolbar (priority)
3521 var topToolbar = document.querySelector('#elementor-editor-wrapper-v2 .MuiToolbar-root');
3522 if (topToolbar) {
3523 var topObserver = createObserver();
3524 topObserver.observe(topToolbar, {
3525 childList: true,
3526 subtree: true
3527 });
3528 }
3529
3530 // Also observe the main editor wrapper for structural changes
3531 var editorWrapper = document.querySelector('#elementor-editor-wrapper-v2');
3532 if (editorWrapper) {
3533 var wrapperObserver = createObserver();
3534 wrapperObserver.observe(editorWrapper, {
3535 childList: true,
3536 subtree: false
3537 });
3538 }
3539
3540 // Observe changes in the main panel (fallback)
3541 var panel = document.querySelector('#elementor-panel');
3542 if (panel) {
3543 var panelObserver = createObserver();
3544 panelObserver.observe(panel, {
3545 childList: true,
3546 subtree: true
3547 });
3548 }
3549
3550 // Observe the main Elementor editor area
3551 var editorArea = document.querySelector('#elementor-editor-wrapper, .elementor-editor-wrapper');
3552 if (editorArea) {
3553 var editorObserver = createObserver();
3554 editorObserver.observe(editorArea, {
3555 childList: true,
3556 subtree: true
3557 });
3558 }
3559 }
3560
3561 /**
3562 * Initialize the translator
3563 */
3564 function initTranslator() {
3565 // Wait for Elementor to be fully loaded
3566 $(window).on('elementor:init', function() {
3567 // Add small delay to ensure Material UI is rendered
3568 setTimeout(onElementorInit, 500);
3569 });
3570
3571 // Fallback if elementor:init doesn't fire
3572 setTimeout(function() {
3573 onElementorInit();
3574 }, 3000);
3575
3576 // Additional fallback for when Material UI components are ready
3577 setTimeout(function() {
3578 if (!document.querySelector('.king-addons-ai-translator-btn')) {
3579 onElementorInit();
3580 }
3581 }, 5000);
3582 }
3583
3584 // Initialize when DOM is ready
3585 $(document).ready(function() {
3586 initTranslator();
3587
3588 // Global event handler for close buttons (backup protection)
3589 $(document).off('click.aiTranslatorGlobal').on('click.aiTranslatorGlobal', '.king-addons-translator-close-btn', function(e) {
3590 if (translationState.isTranslating) {
3591 stopTranslationProcess();
3592
3593 // Show cancellation notice
3594 var $notice = $('<div style="position: fixed; top: 120px; right: 20px; background: #ff9800; color: white; padding: 8px 12px; border-radius: 4px; font-size: 14px; z-index: 1000000;">Run cancelled</div>');
3595 $('body').append($notice);
3596 setTimeout(function() {
3597 $notice.fadeOut(300, function() {
3598 $notice.remove();
3599 });
3600 }, 2000);
3601 }
3602
3603 // Close popup/overlay
3604 var $popup = $(this).closest('.king-addons-translator-popup');
3605 if ($popup.hasClass('compact')) {
3606 $popup.remove();
3607 } else {
3608 $popup.closest('.king-addons-translator-overlay').remove();
3609 }
3610
3611 // Reset state and re-enable button. Saved progress is deliberately
3612 // kept so a cancelled run can be resumed from the same place.
3613 translationState.isTranslating = false;
3614 translationState.isCancelled = false;
3615 translationState.currentRequests = [];
3616 toggleTranslatorButton(false);
3617
3618 e.preventDefault();
3619 e.stopPropagation();
3620 });
3621 });
3622
3623 })(jQuery, window.elementor);