PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.63
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.63
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.63, at includes/admin/js/ai-page-translator.js

2,793 lines 119.0 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 };
21
22 // Language options
23 var languages = {
24 'en': 'English',
25 'es': 'Spanish (Español)',
26 'fr': 'French (Français)',
27 'de': 'German (Deutsch)',
28 'it': 'Italian (Italiano)',
29 'pt': 'Portuguese (Português)',
30 'ru': 'Russian (Русский)',
31 'ja': 'Japanese (日本語)',
32 'ko': 'Korean (한국어)',
33 'zh': 'Chinese (中文)',
34 'ar': 'Arabic (العربية)',
35 'hi': 'Hindi (हिन्दी)',
36 'nl': 'Dutch (Nederlands)',
37 'pl': 'Polish (Polski)',
38 'tr': 'Turkish (Türkçe)',
39 'uk': 'Ukrainian (Українська)',
40 'cs': 'Czech (Čeština)',
41 'sv': 'Swedish (Svenska)',
42 'no': 'Norwegian (Norsk)',
43 'da': 'Danish (Dansk)',
44 'fi': 'Finnish (Suomi)'
45 };
46
47 /**
48 * Check if premium version is active
49 */
50 function isPremiumActive() {
51 // Check for premium indicators
52 return !!(
53 window.KingAddonsPro ||
54 window.kingAddonsPro ||
55 (window.KingAddonsAiField && window.KingAddonsAiField.is_pro) ||
56 (window.KingAddonsAiField && window.KingAddonsAiField.premium_active) ||
57 document.querySelector('body.king-addons-pro') ||
58 (typeof jQuery !== 'undefined' && jQuery('body').hasClass('king-addons-pro'))
59 );
60 }
61
62 /**
63 * Inject CSS styles for the translator
64 */
65 function injectTranslatorStyles() {
66 if ($('#king-addons-ai-translator-styles').length === 0) {
67 const styles = `
68 <style id="king-addons-ai-translator-styles">
69 /* Translator Button Styles */
70 .king-addons-ai-translator-btn {
71 background: linear-gradient(135deg, #E1CBFF, #5B03FF) !important;
72 border: none !important;
73 color: white !important;
74 padding: 8px 12px !important;
75 border-radius: 6px !important;
76 font-size: 12px !important;
77 font-weight: 500 !important;
78 cursor: pointer !important;
79 display: inline-flex !important;
80 align-items: center !important;
81 gap: 6px !important;
82 transition: all 0.3s ease !important;
83 margin: 8px !important;
84 position: relative !important;
85 z-index: 10 !important;
86 text-decoration: none !important;
87 outline: none !important;
88 box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
89 }
90 .king-addons-ai-translator-btn:hover {
91 background: linear-gradient(135deg, #d4a3ff, #4f00e6) !important;
92 box-shadow: 0 4px 12px rgba(91,3,255,0.3) !important;
93 transform: translateY(-1px) !important;
94 }
95 .king-addons-ai-translator-btn:active {
96 transform: translateY(0) !important;
97 box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
98 }
99 .king-addons-ai-translator-btn img {
100 width: 16px !important;
101 height: 16px !important;
102 flex-shrink: 0 !important;
103 }
104 .king-addons-ai-translator-btn span {
105 white-space: nowrap !important;
106 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
107 }
108
109 /* Location-specific styles */
110
111 /* In panel header */
112 .king-addons-translator-location-panel-header {
113 position: absolute !important;
114 top: 50% !important;
115 right: 16px !important;
116 transform: translateY(-50%) !important;
117 margin: 0 !important;
118 z-index: 1000 !important;
119 }
120 .king-addons-translator-location-panel-header:hover {
121 transform: translateY(-50%) translateY(-1px) !important;
122 }
123
124 /* In header within panel */
125 .king-addons-translator-location-header-in-panel {
126 margin-left: auto !important;
127 margin-right: 8px !important;
128 }
129
130 /* At top of panel */
131 .king-addons-translator-location-panel-top {
132 width: calc(100% - 16px) !important;
133 margin: 8px !important;
134 justify-content: center !important;
135 }
136
137 /* In general elementor panel */
138 .king-addons-translator-location-elementor-panel {
139 margin: 8px !important;
140 align-self: flex-end !important;
141 }
142
143 /* Toolbar button group integration styles */
144 .king-addons-translator-location-left-group,
145 .king-addons-translator-location-toolbar-stack,
146 .king-addons-translator-location-toolbar,
147 .king-addons-translator-location-grid-stack {
148 /* Material UI button styling is handled in the HTML structure */
149 display: inline-flex !important;
150 }
151
152 /* Additional spacing for toolbar button */
153 .king-addons-translator-location-left-group .king-addons-ai-translator-btn,
154 .king-addons-translator-location-toolbar-stack .king-addons-ai-translator-btn,
155 .king-addons-translator-location-grid-stack .king-addons-ai-translator-btn {
156 margin-left: 8px !important;
157 }
158
159 /* Compact popup styles */
160 .king-addons-translator-popup.compact .king-addons-translator-progress-text {
161 font-size: 14px;
162 margin-bottom: 8px;
163 }
164
165 .king-addons-translator-popup.compact .king-addons-translator-current-element {
166 font-size: 12px;
167 margin-top: 8px;
168 color: #666;
169 overflow: hidden;
170 text-overflow: ellipsis;
171 white-space: nowrap;
172 }
173
174 .king-addons-translator-popup.compact .king-addons-translator-stats {
175 margin: 12px 0;
176 }
177
178 .king-addons-translator-popup.compact .king-addons-translator-stat {
179 margin: 0 8px;
180 }
181
182 .king-addons-translator-popup.compact .king-addons-translator-stat-number {
183 font-size: 18px;
184 }
185
186 .king-addons-translator-popup.compact .king-addons-translator-stat-label {
187 font-size: 11px;
188 }
189
190 /* Compact mode adjustments for custom fields */
191 .king-addons-translator-popup.compact .king-addons-prompt-examples {
192 padding: 6px;
193 margin-top: 4px;
194 }
195
196 .king-addons-translator-popup.compact .king-addons-prompt-examples small {
197 font-size: 10px;
198 }
199
200 .king-addons-translator-popup.compact .king-addons-pro-info {
201 font-size: 11px;
202 margin-top: 8px;
203 padding: 6px 8px;
204 background: #f8f9fa;
205 border-radius: 4px;
206 border-left: 3px solid #5B03FF;
207 }
208
209 /* Element highlighting styles moved to preview iframe */
210
211 /* Ensure button appears properly in all panel locations */
212 #elementor-panel .king-addons-ai-translator-btn,
213 .elementor-panel .king-addons-ai-translator-btn {
214 max-width: 200px !important;
215 overflow: hidden !important;
216 }
217
218 /* Responsive behavior */
219 @media (max-width: 600px) {
220 /* Hide text in panel buttons on small screens */
221 .king-addons-ai-translator-btn span {
222 display: none !important;
223 }
224 .king-addons-ai-translator-btn {
225 padding: 8px !important;
226 min-width: 32px !important;
227 }
228 }
229
230 /* Popup Overlay */
231 .king-addons-translator-overlay {
232 position: fixed;
233 top: 0;
234 left: 0;
235 right: 0;
236 bottom: 0;
237 background: rgba(0,0,0,0.5);
238 z-index: 999999;
239 display: flex;
240 align-items: center;
241 justify-content: center;
242 transition: opacity 0.3s ease;
243 }
244
245 .king-addons-translator-overlay.hiding {
246 opacity: 0;
247 pointer-events: none;
248 }
249
250 /* Popup Container */
251 .king-addons-translator-popup {
252 background: white;
253 padding: 24px;
254 border-radius: 8px;
255 box-shadow: 0 10px 40px rgba(0,0,0,0.3);
256 width: 90%;
257 max-width: 500px;
258 max-height: 80vh;
259 overflow-y: auto;
260 transition: all 0.3s ease;
261 transform: scale(1);
262 }
263
264 /* Compact popup for top-right positioning */
265 .king-addons-translator-popup.compact {
266 position: fixed;
267 top: 80px;
268 right: 20px;
269 width: 350px;
270 max-width: 350px;
271 padding: 16px;
272 z-index: 999999;
273 max-height: 400px;
274 transform: scale(1);
275 box-shadow: 0 8px 32px rgba(0,0,0,0.4);
276 }
277
278 /* Compact popup header */
279 .king-addons-translator-popup.compact h3 {
280 font-size: 16px;
281 margin: 0 0 12px 0;
282 display: flex;
283 justify-content: space-between;
284 align-items: center;
285 }
286
287 /* Close button for compact popup */
288 .king-addons-translator-close-btn {
289 background: none;
290 border: none;
291 font-size: 18px;
292 cursor: pointer;
293 color: #999;
294 width: 24px;
295 height: 24px;
296 display: flex;
297 align-items: center;
298 justify-content: center;
299 border-radius: 3px;
300 }
301
302 .king-addons-translator-close-btn:hover {
303 background: #f0f0f0;
304 color: #333;
305 }
306
307 /* Animation states */
308 .king-addons-translator-popup.moving {
309 transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
310 }
311
312 /* Notification banner animation */
313 @keyframes slideDown {
314 0% {
315 transform: translateY(-100%);
316 opacity: 0;
317 }
318 100% {
319 transform: translateY(0);
320 opacity: 1;
321 }
322 }
323
324 /* Pulse animation for success numbers */
325 @keyframes pulse {
326 0% {
327 transform: scale(1);
328 opacity: 1;
329 }
330 50% {
331 transform: scale(1.1);
332 opacity: 0.8;
333 }
334 100% {
335 transform: scale(1);
336 opacity: 1;
337 }
338 }
339
340 .king-addons-translator-popup h3 {
341 margin: 0 0 20px 0;
342 font-size: 18px;
343 color: #23282d;
344 display: flex;
345 align-items: center;
346 gap: 8px;
347 }
348
349 .king-addons-translator-form {
350 display: flex;
351 flex-direction: column;
352 gap: 16px;
353 }
354
355 .king-addons-translator-field {
356 display: flex;
357 flex-direction: column;
358 gap: 6px;
359 }
360
361 .king-addons-translator-field label {
362 font-weight: 500;
363 color: #555;
364 font-size: 14px;
365 }
366
367 .king-addons-translator-field select {
368 padding: 8px 12px;
369 border: 1px solid #ddd;
370 border-radius: 4px;
371 font-size: 14px;
372 height: auto;
373 }
374
375 .king-addons-translator-field select:focus {
376 border-color: #5B03FF;
377 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
378 outline: none;
379 }
380
381 .king-addons-translator-field input[type="text"] {
382 padding: 8px 12px;
383 border: 1px solid #ddd;
384 border-radius: 4px;
385 font-size: 14px;
386 margin-top: 6px;
387 transition: border-color 0.3s ease, box-shadow 0.3s ease;
388 }
389
390 .king-addons-translator-field input[type="text"]:focus {
391 border-color: #5B03FF;
392 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
393 outline: none;
394 }
395
396 .king-addons-custom-language-field {
397 margin-top: 8px;
398 display: none;
399 animation: slideDown 0.3s ease-out;
400 }
401
402 .king-addons-custom-language-field.show {
403 display: block;
404 }
405
406 .king-addons-custom-language-field input {
407 width: 100%;
408 box-sizing: border-box;
409 }
410
411 .king-addons-custom-language-field label {
412 font-size: 13px;
413 color: #666;
414 margin-bottom: 4px;
415 display: block;
416 }
417
418 .king-addons-pro-badge {
419 background: linear-gradient(135deg, #FFD700, #FFA500);
420 color: #333;
421 font-size: 10px;
422 font-weight: bold;
423 padding: 2px 6px;
424 border-radius: 3px;
425 margin-left: 6px;
426 vertical-align: middle;
427 }
428
429 /* Style for disabled custom option when not premium */
430 .king-addons-translator-field select option[value="custom"]:disabled {
431 color: #999;
432 background-color: #f5f5f5;
433 }
434
435 /* Enhanced styling for custom language fields */
436 .king-addons-custom-language-field.show input:focus {
437 border-color: #5B03FF;
438 box-shadow: 0 0 0 2px rgba(91,3,255,0.1);
439 }
440
441 /* Info text for premium features */
442 .king-addons-pro-info {
443 font-size: 13px;
444 color: #666;
445 margin-top: 4px;
446 font-style: italic;
447 line-height: 1.4;
448 }
449
450 .king-addons-pro-info a {
451 color: #5B03FF;
452 text-decoration: none;
453 font-weight: 500;
454 }
455
456 .king-addons-pro-info a:hover {
457 color: #4f00e6;
458 text-decoration: underline;
459 }
460
461 /* Prompt examples styling */
462 .king-addons-prompt-examples {
463 margin-top: 6px;
464 padding: 8px;
465 background: #f8f9fa;
466 border-left: 3px solid #5B03FF;
467 border-radius: 0 4px 4px 0;
468 }
469
470 .king-addons-prompt-examples small {
471 color: #666;
472 font-size: 11px;
473 line-height: 1.4;
474 display: block;
475 }
476
477 @keyframes slideDown {
478 from {
479 opacity: 0;
480 max-height: 0;
481 transform: translateY(-10px);
482 }
483 to {
484 opacity: 1;
485 max-height: 100px;
486 transform: translateY(0);
487 }
488 }
489
490 /* Loading spinner animation */
491 @keyframes rotate {
492 from {
493 transform: rotate(0deg);
494 }
495 to {
496 transform: rotate(360deg);
497 }
498 }
499
500 /* Error popup specific styles */
501 .king-addons-translator-popup .king-addons-error-icon {
502 width: 60px;
503 height: 60px;
504 background: #f44336;
505 border-radius: 50%;
506 margin: 0 auto 16px;
507 display: flex;
508 align-items: center;
509 justify-content: center;
510 animation: errorPulse 2s ease-in-out infinite;
511 }
512
513 @keyframes errorPulse {
514 0%, 100% {
515 transform: scale(1);
516 box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4);
517 }
518 50% {
519 transform: scale(1.05);
520 box-shadow: 0 0 0 8px rgba(244, 67, 54, 0.1);
521 }
522 }
523
524 .king-addons-translator-actions {
525 display: flex;
526 gap: 12px;
527 margin-top: 8px;
528 }
529
530 .king-addons-translator-btn-primary {
531 background: linear-gradient(135deg, #E1CBFF, #5B03FF);
532 border: none;
533 color: white;
534 padding: 10px 20px;
535 border-radius: 4px;
536 font-size: 14px;
537 font-weight: 500;
538 cursor: pointer;
539 flex: 1;
540 transition: all 0.3s ease;
541 }
542
543 .king-addons-translator-btn-primary:hover {
544 box-shadow: 0 0 12px rgba(91,3,255,0.5);
545 color: #fff;
546 }
547
548 .king-addons-translator-btn-primary:disabled {
549 background: #ccc;
550 cursor: not-allowed;
551 box-shadow: none;
552 }
553
554 .king-addons-translator-btn-secondary {
555 background: #f1f1f1;
556 border: 1px solid #ddd;
557 color: #555;
558 padding: 10px 20px;
559 border-radius: 4px;
560 font-size: 14px;
561 cursor: pointer;
562 flex: 1;
563 transition: all 0.3s ease;
564 }
565
566 .king-addons-translator-btn-secondary:hover {
567 background: #e8e8e8;
568 }
569
570 /* Progress Styles */
571 .king-addons-translator-progress {
572 margin-top: 16px;
573 padding: 16px;
574 background: #f8f9fa;
575 border-radius: 6px;
576 border-left: 4px solid #5B03FF;
577 }
578
579 .king-addons-translator-progress-text {
580 font-size: 14px;
581 color: #555;
582 margin-bottom: 8px;
583 }
584
585 .king-addons-translator-progress-bar {
586 width: 100%;
587 height: 8px;
588 background: #e0e0e0;
589 border-radius: 4px;
590 overflow: hidden;
591 margin-bottom: 8px;
592 }
593
594 .king-addons-translator-progress-fill {
595 height: 100%;
596 background: linear-gradient(90deg, #5B03FF, #E1CBFF);
597 width: 0%;
598 transition: width 0.3s ease;
599 }
600
601 .king-addons-translator-current-element {
602 font-size: 12px;
603 color: #777;
604 font-style: italic;
605 }
606
607 /* Stats Styles */
608 .king-addons-translator-stats {
609 margin-top: 16px;
610 display: grid;
611 grid-template-columns: repeat(3, 1fr);
612 gap: 12px;
613 }
614
615 .king-addons-translator-stat {
616 text-align: center;
617 padding: 12px;
618 background: #f8f9fa;
619 border-radius: 6px;
620 }
621
622 .king-addons-translator-stat-number {
623 font-size: 20px;
624 font-weight: bold;
625 color: #5B03FF;
626 }
627
628 .king-addons-translator-stat-label {
629 font-size: 12px;
630 color: #777;
631 margin-top: 4px;
632 }
633
634 /* Element animations handled in preview iframe */
635 </style>
636 `;
637 $('head').append(styles);
638 }
639 }
640
641 /**
642 * Add translator button to Elementor panel
643 */
644 function addTranslatorButton() {
645 // Check if button already exists
646 if (document.querySelector('.king-addons-ai-translator-btn')) {
647 return;
648 }
649
650 // Strategy 1: Try to find the left button group in the top toolbar (after initial buttons)
651 var $leftButtonGroup = $('#elementor-editor-wrapper-v2 .MuiStack-root.eui-1g5sxhh:first');
652 if ($leftButtonGroup.length) {
653 return addButtonToElement($leftButtonGroup, 'left-group');
654 }
655
656 // Strategy 2: Try to find the first stack group in the toolbar
657 var $toolbarStack = $('#elementor-editor-wrapper-v2 .MuiStack-root');
658 if ($toolbarStack.length) {
659 return addButtonToElement($toolbarStack.first(), 'toolbar-stack');
660 }
661
662 // Strategy 2.5: Try to find grid container with stacks
663 var $gridContainer = $('#elementor-editor-wrapper-v2 .MuiGrid-container:first');
664 if ($gridContainer.length) {
665 var $firstStack = $gridContainer.find('.MuiStack-root:first');
666 if ($firstStack.length) {
667 return addButtonToElement($firstStack, 'grid-stack');
668 }
669 }
670
671 // Strategy 3: Try to find the toolbar itself
672 var $toolbar = $('#elementor-editor-wrapper-v2 .MuiToolbar-root');
673 if ($toolbar.length) {
674 return addButtonToElement($toolbar, 'toolbar');
675 }
676
677 // Strategy 4: Try to find the Elementor panel header (fallback)
678 var $panelHeader = $('#elementor-panel-header');
679 if ($panelHeader.length) {
680 return addButtonToElement($panelHeader, 'panel-header');
681 }
682
683 // Strategy 5: Try to find the main panel (further fallback)
684 var $panel = $('#elementor-panel');
685 if ($panel.length) {
686 return addButtonToElement($panel, 'panel-fallback');
687 }
688 return false;
689 }
690
691 /**
692 * Helper function to add button to specific element
693 */
694 function addButtonToElement($target, location) {
695 // Try using the custom icon, fallback to the standard AI icon
696 var iconUrl = KingAddonsAiField.plugin_url + 'includes/admin/img/ai.svg';
697 var fallbackIconUrl = KingAddonsAiField.plugin_url + 'includes/admin/img/ai.svg';
698
699 var $translatorBtn;
700
701 // Create button with appropriate styling based on location
702 if (location === 'left-group' || location === 'toolbar-stack' || location === 'toolbar' || location === 'grid-stack') {
703 // Material UI style button for toolbar with text and custom icon
704 $translatorBtn = $('<span class="MuiBox-root eui-0">' +
705 '<button class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textInherit MuiButton-sizeSmall MuiButton-textSizeSmall MuiButton-colorInherit king-addons-ai-translator-btn eui-17yw4pm" ' +
706 'tabindex="0" type="button" aria-label="AI Page Translator" title="AI Page Translator">' +
707 '<span class="MuiButton-startIcon MuiButton-iconSizeSmall" style="margin-right: 4px;">' +
708 '<img src="' + iconUrl + '" alt="AI" onerror="this.src=\'' + fallbackIconUrl + '\'" style="width: 20px; height: 20px;" />' +
709 '</span>' +
710 '<span class="MuiStack-root" style="color: white;">AI Page Translator</span>' +
711 '</button>' +
712 '</span>');
713 } else {
714 // Original button style for panel locations
715 $translatorBtn = $('<button class="king-addons-ai-translator-btn" title="AI Page Translator">' +
716 '<img src="' + iconUrl + '" alt="AI Page Translator" onerror="this.src=\'' + fallbackIconUrl + '\'"/>' +
717 '<span>AI Page Translator</span>' +
718 '</button>');
719 }
720
721 // Add location-specific class for different styling if needed
722 $translatorBtn.addClass('king-addons-translator-location-' + location);
723
724 // Add to the target element based on location
725 if (location === 'panel-top' || location === 'panel-fallback') {
726 $target.prepend($translatorBtn);
727 } else if (location === 'left-group' || location === 'toolbar-stack' || location === 'grid-stack') {
728 // Add after existing buttons in the left group
729 $target.append($translatorBtn);
730 } else {
731 $target.append($translatorBtn);
732 }
733
734 // Bind click event (works for both button structures)
735 $translatorBtn.find('button').length ?
736 $translatorBtn.find('button').on('click', handleButtonClick) :
737 $translatorBtn.on('click', handleButtonClick);
738
739 function handleButtonClick(e) {
740 e.preventDefault();
741 // Check if button is disabled or translation is in progress
742 if (translationState.isTranslating || $(e.currentTarget).prop('disabled')) {
743 return;
744 }
745 showTranslatorPopup();
746 }
747
748 // Add debug info to button
749 var $btn = $translatorBtn.find('button').length ? $translatorBtn.find('button') : $translatorBtn;
750 $btn.attr('data-location', location);
751 $btn.attr('data-target', $target.prop('tagName') + ($target.attr('id') ? '#' + $target.attr('id') : '') + ($target.attr('class') ? '.' + $target.attr('class').split(' ').join('.') : ''));
752
753 return true;
754 }
755
756 /**
757 * Show the translator popup
758 */
759 function showTranslatorPopup() {
760 // Check API key first
761 checkApiKeyAndShowPopup();
762 }
763
764 /**
765 * Check API key before showing popup
766 */
767 function checkApiKeyAndShowPopup() {
768 // Show loading state briefly
769 var $loadingOverlay = showLoadingOverlay();
770
771 $.post(KingAddonsAiField.ajax_url, {
772 action: 'king_addons_ai_check_tokens',
773 nonce: KingAddonsAiField.generate_nonce
774 }, function(response) {
775 $loadingOverlay.remove();
776
777 if (!response.success) {
778 if (response.data && response.data.message) {
779 var errorMessage = response.data.message;
780
781 // Check for token limit errors first
782 if (errorMessage.toLowerCase().includes('token limit') ||
783 errorMessage.toLowerCase().includes('daily limit') ||
784 errorMessage.toLowerCase().includes('limit reached') ||
785 errorMessage.toLowerCase().includes('quota exceeded') ||
786 errorMessage.toLowerCase().includes('rate limit')) {
787
788 showTokenLimitError(errorMessage);
789 return;
790 }
791
792 showApiKeyError('API Error', errorMessage);
793 } else {
794 showApiKeyError('Connection Issue', 'Unable to connect right now. Please check your internet connection and try again.');
795 }
796 return;
797 }
798
799 if (!response.data.api_key_valid) {
800 var errorMessage = response.data.error_message || 'API key is missing or invalid';
801
802 // Check for token limit errors first
803 if (errorMessage.toLowerCase().includes('token limit') ||
804 errorMessage.toLowerCase().includes('daily limit') ||
805 errorMessage.toLowerCase().includes('limit reached') ||
806 errorMessage.toLowerCase().includes('quota exceeded') ||
807 errorMessage.toLowerCase().includes('rate limit') ||
808 errorMessage.toLowerCase().includes('too many requests')) {
809
810 showTokenLimitError(errorMessage);
811 return;
812 }
813
814 showApiKeyError('API Key Required', errorMessage);
815 return;
816 }
817
818 createAndShowPopup();
819 }).fail(function(xhr) {
820 $loadingOverlay.remove();
821
822 if (xhr.status === 0) {
823 showApiKeyError('Connection Issue', 'Network connection failed. Please check your internet connection and try again.');
824 } else {
825 showApiKeyError('Temporary Issue', 'Server is temporarily unavailable. Please try again in a few minutes.');
826 }
827 });
828 }
829
830 /**
831 * Show loading overlay
832 */
833 function showLoadingOverlay() {
834 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
835 var $popup = $('<div class="king-addons-translator-popup" style="text-align: center; padding: 40px;"></div>');
836
837 var loadingHtml = `
838 <div style="margin-bottom: 16px;">
839 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:40px;height:40px;filter: invert(1); animation: rotate 1s linear infinite;"/>
840 </div>
841 <div style="font-size: 16px; color: #333; margin-bottom: 8px;">🔍 Verifying Setup...</div>
842 <div style="font-size: 12px; color: #666;">Just checking that everything is ready for translation</div>
843 `;
844
845 $popup.html(loadingHtml);
846 $overlay.append($popup);
847 $('body').append($overlay);
848
849 return $overlay;
850 }
851
852 /**
853 * Show API key error with detailed information
854 */
855 function showApiKeyError(title, message) {
856 // Aggressively remove any existing popups/overlays
857 $('.king-addons-translator-overlay').remove();
858 $('.king-addons-translator-popup').remove();
859
860 // Wait a bit to ensure cleanup is complete
861 setTimeout(function() {
862 showApiKeyErrorDelayed(title, message);
863 }, 100);
864 }
865
866 function showApiKeyErrorDelayed(title, message) {
867 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
868 ? window.KingAddonsAiField.settings_url
869 : '/wp-admin/admin.php?page=king-addons-ai-settings';
870
871 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
872 var $popup = $('<div class="king-addons-translator-popup"></div>');
873
874 var errorHtml = `
875 <div style="text-align: center; margin-bottom: 32px;">
876 <h3 style="margin: 0 0 16px 0; color: #2d3748; font-size: 20px; text-align: center;justify-content: center;">AI Translator Setup Required</h3>
877 <p style="color: #718096; margin: 0; font-size: 14px;">This is completely safe and takes only 2 minutes!</p>
878 </div>
879
880 <div style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border: 1px solid #e2e8f0; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
881 <h4 style="color: #2d3748; margin: 0 0 12px 0; font-size: 16px;">📋 What you need to do:</h4>
882
883 <div style="margin-bottom: 12px; display: flex; align-items: center;">
884 <span style="background: #48bb78; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 12px; font-weight: bold;max-width: 24px;flex-basis: 46px;">1</span>
885 <span style="color: #4a5568;">Get an API key from <a href="https://platform.openai.com/api-keys" target="_blank" style="color: #5B03FF; text-decoration: none;line-height: 1.4;">OpenAI Platform</a> and top up your OpenAI account balance by at least $5</span>
886 </div>
887
888 <div style="margin-bottom: 12px; display: flex; align-items: center;">
889 <span style="background: #48bb78; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 12px; font-weight: bold;">2</span>
890 <span style="color: #4a5568;">Paste it in AI Settings</span>
891 </div>
892
893 <div style="margin-bottom: 12px; display: flex; align-items: center;">
894 <span style="background: #48bb78; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 12px; font-weight: bold;">3</span>
895 <span style="color: #4a5568;">Done! Translate as much as you want 🎉</span>
896 </div>
897 </div>
898
899 <div style="background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; margin-bottom: 20px;">
900 <div style="display: flex; align-items: center; margin-bottom: 8px;">
901 <span style="color: #495057; margin-right: 8px; font-size: 16px;">💰</span>
902 <strong style="color: #495057;">How much does it cost?</strong>
903 </div>
904 <p style="color: #6c757d; margin: 0; font-size: 14px; line-height: 1.4;">
905 Translation costs pennies (about $0.01 per full page).
906 </p>
907 </div>
908
909 <div style="display: flex; gap: 12px;">
910 <button class="king-addons-translator-btn-secondary" id="king-addons-error-close" style="flex: 1;">
911 Not now
912 </button>
913 <a href="${settingsUrl}" class="king-addons-translator-btn-primary" style="flex: 1; text-decoration: none; display: flex; align-items: center; justify-content: center;">
914 Set up now
915 </a>
916 </div>
917 `;
918
919 $popup.html(errorHtml);
920 $overlay.append($popup);
921 $('body').append($overlay);
922
923 // Bind close event
924 $('#king-addons-error-close').on('click', function() {
925 $overlay.remove();
926 });
927
928 // Close on overlay click
929 $overlay.on('click', function(e) {
930 if (e.target === $overlay[0]) {
931 $overlay.remove();
932 }
933 });
934 }
935
936 /**
937 * Show token limit error popup
938 */
939 function showTokenLimitError(message) {
940 // Remove any existing popups first
941 $('.king-addons-translator-overlay').remove();
942 $('.king-addons-translator-popup').remove();
943
944 // Wait a bit to ensure cleanup is complete
945 setTimeout(function() {
946 showTokenLimitErrorDelayed(message);
947 }, 100);
948 }
949
950 function showTokenLimitErrorDelayed(message) {
951 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
952 ? window.KingAddonsAiField.settings_url
953 : '/wp-admin/admin.php?page=king-addons-ai-settings';
954
955 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
956 var $popup = $('<div class="king-addons-translator-popup"></div>');
957
958 var errorHtml = `
959 <div style="text-align: center; margin-bottom: 32px;">
960 <h3 style="margin: 0 0 16px 0; color: #2d3748; font-size: 20px; text-align: center;justify-content: center;">🛡️ Daily Limit Reached</h3>
961 <p style="color: #718096; margin: 0; font-size: 14px;">Your safety limit is protecting your account!</p>
962 </div>
963
964 <div style="background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%); border: 1px solid #c3e6cb; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
965 <h4 style="color: #155724; margin: 0 0 12px 0; font-size: 16px;">🎯 What's happening?</h4>
966 <p style="color: #155724; margin: 0 0 16px 0; font-size: 14px; line-height: 1.5;">
967 You have a <strong>"Daily Token Limit"</strong> setting that prevents accidental overspending.
968 This is a <em>good thing</em> - it's working as intended to protect your account!
969 </p>
970
971 <h4 style="color: #155724; margin: 0 0 12px 0; font-size: 16px;">⚙️ Easy solutions:</h4>
972
973 <div style="margin-bottom: 12px; display: flex; align-items: center;">
974 <span style="background: #28a745; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 12px; font-weight: bold;">1</span>
975 <span style="color: #155724;"><strong>Increase the "Daily Token Limit"</strong> in AI Settings (recommended)</span>
976 </div>
977
978 <div style="margin-bottom: 12px; display: flex; align-items: center;">
979 <span style="background: #28a745; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 12px; font-weight: bold;">2</span>
980 <span style="color: #155724;">Or wait until tomorrow (limit automatically resets)</span>
981 </div>
982 </div>
983
984 <div style="background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; margin-bottom: 20px;">
985 <div style="display: flex; align-items: center; margin-bottom: 8px;">
986 <span style="color: #495057; margin-right: 8px; font-size: 16px;">💡</span>
987 <strong style="color: #495057;">Quick tip</strong>
988 </div>
989 <p style="color: #6c757d; margin: 0; font-size: 14px; line-height: 1.4;">
990 Just go to <strong>AI Settings → Daily Token Limit</strong> and set a higher number.
991 For regular use, try setting it to <strong>50,000 or 100,000 tokens</strong>.
992 </p>
993 </div>
994
995 <div style="display: flex; gap: 12px;">
996 <button class="king-addons-translator-btn-secondary" id="king-addons-limit-close" style="flex: 1;">
997 I understand
998 </button>
999 <a href="${settingsUrl}" class="king-addons-translator-btn-primary" style="flex: 1; text-decoration: none; display: flex; align-items: center; justify-content: center;">
1000 Open AI Settings
1001 </a>
1002 </div>
1003 `;
1004
1005 $popup.html(errorHtml);
1006 $overlay.append($popup);
1007 $('body').append($overlay);
1008
1009 // Bind close event
1010 $('#king-addons-limit-close').on('click', function() {
1011 $overlay.remove();
1012 });
1013
1014 // Close on overlay click
1015 $overlay.on('click', function(e) {
1016 if (e.target === $overlay[0]) {
1017 $overlay.remove();
1018 }
1019 });
1020 }
1021
1022 /**
1023 * Disable/enable the AI Translator button
1024 */
1025 function toggleTranslatorButton(disabled) {
1026 var $button = $('.king-addons-ai-translator-btn');
1027
1028 if (disabled) {
1029 $button.prop('disabled', true);
1030 $button.css('opacity', '0.5');
1031 $button.css('cursor', 'not-allowed');
1032 } else {
1033 $button.prop('disabled', false);
1034 $button.css('opacity', '1');
1035 $button.css('cursor', 'pointer');
1036 }
1037 }
1038
1039 /**
1040 * Stop the translation process
1041 */
1042 function stopTranslationProcess() {
1043 // Prevent multiple calls
1044 if (translationState.isCancelled) {
1045 return;
1046 }
1047
1048 translationState.isCancelled = true;
1049 translationState.isTranslating = false;
1050
1051 // Cancel all active AJAX requests
1052 if (translationState.currentRequests.length > 0) {
1053 translationState.currentRequests.forEach(function(request) {
1054 if (request && request.abort) {
1055 request.abort();
1056 }
1057 });
1058 translationState.currentRequests = [];
1059 }
1060
1061 // Remove any highlighting from current element
1062 if (translationState.currentElement) {
1063 highlightElementInPreview(translationState.currentElement.elementId, false);
1064 }
1065
1066 // Remove any existing popups/overlays
1067 $('.king-addons-translator-overlay').remove();
1068
1069 // Re-enable the button
1070 toggleTranslatorButton(false);
1071 }
1072
1073 /**
1074 * Animate popup to top-right corner
1075 */
1076 function movePopupToCorner($popup, $overlay) {
1077 return new Promise(function(resolve) {
1078 // Add moving class for smooth animation
1079 $popup.addClass('moving');
1080
1081 // Hide overlay with fade
1082 $overlay.addClass('hiding');
1083
1084 // Calculate current position and target position
1085 var currentRect = $popup[0].getBoundingClientRect();
1086 var targetTop = 80;
1087 var targetRight = 20;
1088 var targetLeft = window.innerWidth - 350 - 20;
1089
1090 // Move popup from overlay to body with current position
1091 $popup.css({
1092 'position': 'fixed',
1093 'top': currentRect.top + 'px',
1094 'left': currentRect.left + 'px',
1095 'width': currentRect.width + 'px',
1096 'margin': '0',
1097 'transform': 'none',
1098 'z-index': 999999
1099 });
1100
1101 // Append popup to body (remove from overlay)
1102 $('body').append($popup);
1103
1104 // Wait for overlay to fade, then animate popup
1105 setTimeout(function() {
1106 // Force reflow
1107 $popup[0].offsetHeight;
1108
1109 // Animate to final position
1110 $popup.css({
1111 'top': targetTop + 'px',
1112 'left': targetLeft + 'px',
1113 'width': '350px',
1114 'padding': '16px'
1115 });
1116
1117 // Add compact class after animation and remove overlay
1118 setTimeout(function() {
1119 $popup.removeClass('moving').addClass('compact');
1120 $overlay.remove(); // Remove overlay completely
1121 resolve();
1122 }, 500);
1123
1124 }, 300);
1125 });
1126 }
1127
1128 /**
1129 * Create and show the main popup
1130 */
1131 function createAndShowPopup() {
1132 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1133 var $popup = $('<div class="king-addons-translator-popup"></div>');
1134
1135 var isPro = isPremiumActive();
1136 var customOptionHtml = isPro ?
1137 '<option value="custom">Custom language or prompt (PRO)</option>' :
1138 '<option value="custom" disabled>Custom language or prompt (PRO)</option>';
1139
1140 var upgradeUrl = 'https://kingaddons.com/pricing/?utm_source=ai-translator&utm_medium=plugin&utm_campaign=custom-prompts';
1141 var proInfoHtml = isPro ?
1142 '<div class="king-addons-pro-info" style="color: #4CAF50; border-left-color: #4CAF50;">�
1143 PRO Active: Use custom languages and translation prompts!</div>' :
1144 '<div class="king-addons-pro-info">💎 <a href="' + upgradeUrl + '" target="_blank" style="color: #5B03FF; text-decoration: none;">Upgrade to PRO</a> to use custom languages, regional dialects and custom translation prompts (formal tone, technical style, etc.)!</div>';
1145
1146 var popupContent = `
1147 <h3>
1148 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);"/>
1149 AI Page Translator
1150 </h3>
1151 <div style="font-size: 12px; color: #666; margin-bottom: 16px; line-height: 1.4;">
1152 Translate to any language or transform text style (formal, casual, technical, etc.)
1153 </div>
1154 <div class="king-addons-translator-form">
1155 <div class="king-addons-translator-field">
1156 <label>From Language</label>
1157 <select id="king-addons-from-lang">
1158 <option value="auto">Auto-detect</option>
1159 ${Object.keys(languages).map(code =>
1160 `<option value="${code}">${languages[code]}</option>`
1161 ).join('')}
1162 ${customOptionHtml}
1163 </select>
1164 <div class="king-addons-custom-language-field" id="king-addons-custom-from-field">
1165 <label>Custom language or translation prompt</label>
1166 <input type="text" id="king-addons-custom-from-lang" placeholder="e.g., Klingon, Old English, formal business tone, medical terminology..." />
1167 <div class="king-addons-prompt-examples">
1168 <small>Examples: "Klingon", "Shakespeare English", "formal business style", "casual conversational tone"</small>
1169 </div>
1170 </div>
1171 </div>
1172 <div class="king-addons-translator-field">
1173 <label>To Language</label>
1174 <select id="king-addons-to-lang">
1175 ${Object.keys(languages).map(code =>
1176 `<option value="${code}" ${code === 'en' ? 'selected' : ''}>${languages[code]}</option>`
1177 ).join('')}
1178 ${customOptionHtml}
1179 </select>
1180 <div class="king-addons-custom-language-field" id="king-addons-custom-to-field">
1181 <label>Custom language or translation style</label>
1182 <input type="text" id="king-addons-custom-to-lang" placeholder="e.g., Dothraki, Academic writing, pirate speak, baby talk..." />
1183 <div class="king-addons-prompt-examples">
1184 <small>Examples: "Dothraki", "academic paper style", "pirate language", "simplified for children"</small>
1185 </div>
1186 </div>
1187 </div>
1188 ${proInfoHtml}
1189 <div class="king-addons-translator-actions">
1190 <button class="king-addons-translator-btn-secondary" id="king-addons-cancel-translation">
1191 Cancel
1192 </button>
1193 <button class="king-addons-translator-btn-primary" id="king-addons-start-translation">
1194 Start Translation
1195 </button>
1196 </div>
1197 </div>
1198 `;
1199
1200 $popup.html(popupContent);
1201 $overlay.append($popup);
1202 $('body').append($overlay);
1203
1204 // Store references globally
1205 window.currentTranslatorPopup = $popup;
1206 window.currentTranslatorOverlay = $overlay;
1207
1208 // Bind events for language selection
1209 bindLanguageSelectionEvents();
1210
1211 // Bind events
1212 $('#king-addons-cancel-translation').on('click', function() {
1213 if (!translationState.isTranslating) {
1214 $overlay.remove();
1215 }
1216 });
1217
1218 $('#king-addons-start-translation').on('click', function() {
1219 var result = getSelectedLanguages();
1220
1221 if (!result.valid) {
1222 alert(result.error);
1223 return;
1224 }
1225
1226 // Double-check API key before starting translation
1227 var $button = $(this);
1228 $button.prop('disabled', true).text('🔍 Verifying...');
1229
1230 $.post(KingAddonsAiField.ajax_url, {
1231 action: 'king_addons_ai_check_tokens',
1232 nonce: KingAddonsAiField.generate_nonce
1233 }, function(response) {
1234 $button.prop('disabled', false).text('Start Translation');
1235
1236 if (!response.success || !response.data.api_key_valid) {
1237 var errorMessage = 'API key verification failed. Please check your API key in settings.';
1238 if (response.data && response.data.error_message) {
1239 errorMessage = response.data.error_message;
1240 }
1241
1242 // Check for token limit errors first
1243 if (errorMessage.toLowerCase().includes('token limit') ||
1244 errorMessage.toLowerCase().includes('daily limit') ||
1245 errorMessage.toLowerCase().includes('limit reached') ||
1246 errorMessage.toLowerCase().includes('quota exceeded') ||
1247 errorMessage.toLowerCase().includes('rate limit') ||
1248 errorMessage.toLowerCase().includes('too many requests')) {
1249
1250 // Show token limit error popup
1251 showTokenLimitError(errorMessage);
1252 return;
1253 }
1254
1255 // Show error popup (will handle cleanup automatically)
1256 showApiKeyError('Setup Required', errorMessage);
1257 return;
1258 }
1259
1260 // API key is valid, proceed with translation
1261 startTranslation(result.fromLang, result.toLang, $popup, $overlay);
1262
1263 }).fail(function() {
1264 $button.prop('disabled', false).text('Start Translation');
1265
1266 // Show error popup (will handle cleanup automatically)
1267 showApiKeyError('Connection Issue', 'Failed to connect right now. Please check your connection and try again.');
1268 });
1269 });
1270
1271 // Close on overlay click only if not translating
1272 $overlay.on('click', function(e) {
1273 if (e.target === $overlay[0] && !translationState.isTranslating) {
1274 $overlay.remove();
1275 }
1276 });
1277 }
1278
1279 /**
1280 * Bind events for language selection dropdowns
1281 */
1282 function bindLanguageSelectionEvents() {
1283 // Handle From Language selection
1284 $('#king-addons-from-lang').on('change', function() {
1285 var selectedValue = $(this).val();
1286 var $customField = $('#king-addons-custom-from-field');
1287
1288 if (selectedValue === 'custom') {
1289 if (!isPremiumActive()) {
1290 // Reset to previous value and show upgrade message
1291 $(this).val('auto');
1292 alert('Custom languages and translation prompts are a PRO feature. Please upgrade to King Addons PRO to use custom languages or translation styles.');
1293 return;
1294 }
1295 $customField.addClass('show');
1296 $('#king-addons-custom-from-lang').focus();
1297 } else {
1298 $customField.removeClass('show');
1299 }
1300 });
1301
1302 // Handle To Language selection
1303 $('#king-addons-to-lang').on('change', function() {
1304 var selectedValue = $(this).val();
1305 var $customField = $('#king-addons-custom-to-field');
1306
1307 if (selectedValue === 'custom') {
1308 if (!isPremiumActive()) {
1309 // Reset to previous value and show upgrade message
1310 $(this).val('en');
1311 alert('Custom languages and translation prompts are a PRO feature. Please upgrade to King Addons PRO to use custom languages or translation styles.');
1312 return;
1313 }
1314 $customField.addClass('show');
1315 $('#king-addons-custom-to-lang').focus();
1316 } else {
1317 $customField.removeClass('show');
1318 }
1319 });
1320 }
1321
1322 /**
1323 * Get selected languages with validation
1324 */
1325 function getSelectedLanguages() {
1326 var fromLang = $('#king-addons-from-lang').val();
1327 var toLang = $('#king-addons-to-lang').val();
1328 var customFromLang = $('#king-addons-custom-from-lang').val().trim();
1329 var customToLang = $('#king-addons-custom-to-lang').val().trim();
1330
1331 // Handle custom from language
1332 if (fromLang === 'custom') {
1333 if (!customFromLang) {
1334 return {
1335 valid: false,
1336 error: 'Please enter a custom source language or translation prompt.'
1337 };
1338 }
1339 fromLang = customFromLang;
1340 }
1341
1342 // Handle custom to language
1343 if (toLang === 'custom') {
1344 if (!customToLang) {
1345 return {
1346 valid: false,
1347 error: 'Please enter a custom target language or translation style.'
1348 };
1349 }
1350 toLang = customToLang;
1351 }
1352
1353 // Validate languages are different (except auto-detect)
1354 if (fromLang === toLang && fromLang !== 'auto') {
1355 return {
1356 valid: false,
1357 error: 'Source and target languages cannot be the same.'
1358 };
1359 }
1360
1361 return {
1362 valid: true,
1363 fromLang: fromLang,
1364 toLang: toLang
1365 };
1366 }
1367
1368 /**
1369 * Start the translation process
1370 */
1371 function startTranslation(fromLang, toLang, $popup, $overlay) {
1372 translationState.isTranslating = true;
1373 translationState.isCancelled = false; // Reset cancellation flag
1374 translationState.currentRequests = []; // Clear any previous requests
1375 translationState.fromLang = fromLang;
1376 translationState.toLang = toLang;
1377 translationState.translatedElements = 0;
1378 translationState.failedElements = 0;
1379
1380 // Inject animation styles into preview iframe immediately
1381 injectPreviewStyles();
1382
1383 // Disable the AI Translator button
1384 toggleTranslatorButton(true);
1385
1386 // Get all translatable elements
1387 var elements = getTranslatableElements();
1388 translationState.totalElements = elements.length;
1389
1390 if (elements.length === 0) {
1391 alert('No translatable text elements found on this page.');
1392 translationState.isTranslating = false;
1393 toggleTranslatorButton(false);
1394 return;
1395 }
1396
1397 // Update popup to show progress
1398 showProgressInPopup($popup);
1399
1400 // Animate popup to corner and start translation
1401 movePopupToCorner($popup, $overlay).then(function() {
1402 // Start translating elements one by one
1403 translateElementsSequentially(elements, 0, $popup);
1404 });
1405 }
1406
1407 /**
1408 * Get all translatable text elements
1409 */
1410 function getTranslatableElements() {
1411 var elements = [];
1412 // Get the main document container using Elementor 3.0+ API
1413 var documentContainer = elementor.documents.getCurrent().container;
1414 var elementorElements = [];
1415
1416 // Use the new API to get children - for Elementor 3.0+
1417 if (documentContainer.children && typeof documentContainer.children.models !== 'undefined') {
1418 // Backbone collection - extract models
1419 elementorElements = documentContainer.children.models || [];
1420 } else if (documentContainer.elements && typeof documentContainer.elements.models !== 'undefined') {
1421 // Alternative property name in some Elementor versions
1422 elementorElements = documentContainer.elements.models || [];
1423 } else if (Array.isArray(documentContainer.children)) {
1424 // Fallback for older API
1425 elementorElements = documentContainer.children;
1426 } else {
1427 console.warn('🚨 Unable to find container children using any known API');
1428 elementorElements = [];
1429 }
1430
1431 function processContainer(container) {
1432 var model = container.model;
1433 var elementType = model.get('elType');
1434 var widgetType = model.get('widgetType');
1435
1436 // Process text-based widgets
1437 if (widgetType) {
1438 // First, check if this widget type should be skipped entirely
1439 var nonTextWidgets = [
1440 'spacer', 'divider', 'html', 'shortcode', 'sidebar',
1441 'menu-anchor', 'read-more', 'google_maps', 'paypal_button',
1442 'stripe_button', 'facebook_button', 'facebook_page',
1443 'video', 'audio', 'iframe', 'code', 'wp-widget',
1444 'map', 'rating', 'progress', 'counter', 'countdown',
1445 'social-icons', 'share-buttons', 'login', 'lottie',
1446 'image' // Image widget should be skipped
1447 ];
1448
1449 if (nonTextWidgets.indexOf(widgetType) !== -1) {
1450 return; // Exit early for blacklisted widgets
1451 }
1452
1453 var settings = model.get('settings').attributes;
1454
1455 // Now check for text fields in remaining widgets
1456 var textFields = getTextFieldsForWidget(widgetType, settings, container);
1457
1458 // If we found text fields, process the widget
1459 if (textFields.length > 0) {
1460 elements.push({
1461 container: container,
1462 widgetType: widgetType,
1463 textFields: textFields,
1464 elementId: model.get('id')
1465 });
1466 return;
1467 }
1468 }
1469
1470 // Process child containers recursively using Elementor 3.0+ API
1471 if (container.children && container.children.length > 0) {
1472 // Check if children is a Backbone collection
1473 if (typeof container.children.models !== 'undefined') {
1474 container.children.models.forEach(processContainer);
1475 } else if (Array.isArray(container.children)) {
1476 container.children.forEach(processContainer);
1477 }
1478 }
1479 }
1480
1481 elementorElements.forEach(processContainer);
1482 return elements;
1483 }
1484
1485 /**
1486 * Get text fields for a specific widget type using Elementor control types
1487 */
1488 function getTextFieldsForWidget(widgetType, settings, container) {
1489 var textFields = [];
1490
1491 // Try to get widget controls schema from Elementor
1492 var controls = getWidgetControls(widgetType, container);
1493
1494 if (controls && Object.keys(controls).length > 0) {
1495 // Look for text-based controls
1496 Object.keys(controls).forEach(function(controlName) {
1497 var control = controls[controlName];
1498 var controlType = control.type;
1499 var settingValue = settings[controlName];
1500
1501 // Check if this is a text-based control type
1502 var textControlTypes = [
1503 'text', 'textarea', 'wysiwyg', 'url', 'email',
1504 'password', 'search', 'tel', 'date', 'time',
1505 'datetime-local', 'month', 'week'
1506 ];
1507
1508 if (textControlTypes.includes(controlType)) {
1509 // Check if the field has a non-empty string value
1510 if (settingValue && typeof settingValue === 'string' && settingValue.trim()) {
1511 // Skip obviously non-translatable fields
1512 var skipFields = [
1513 '_element_id', '_css_classes', 'link', 'url', 'href',
1514 'custom_css', 'css_id', 'anchor', 'html_tag'
1515 ];
1516
1517 if (!skipFields.includes(controlName)) {
1518 textFields.push({
1519 field: controlName,
1520 value: settingValue,
1521 type: controlType === 'wysiwyg' ? 'wysiwyg' : 'text'
1522 });
1523 }
1524 }
1525 }
1526
1527 // Also check for repeater controls
1528 if (controlType === 'repeater' && settingValue) {
1529 checkRepeaterFieldsByType(controlName, control, settingValue, textFields);
1530 }
1531 });
1532 } else {
1533 // Fallback: Use the original method for widgets without accessible controls
1534 var commonTextFields = [
1535 'title', 'text', 'content', 'description', 'subtitle', 'button_text',
1536 'heading_title', 'heading_subtitle', 'testimonial_content', 'testimonial_name',
1537 'title_text', 'description_text', 'content_text', 'editor'
1538 ];
1539
1540 commonTextFields.forEach(function(field) {
1541 if (settings[field] && typeof settings[field] === 'string' && settings[field].trim()) {
1542 textFields.push({
1543 field: field,
1544 value: settings[field],
1545 type: field === 'editor' ? 'wysiwyg' : 'text'
1546 });
1547 }
1548 });
1549
1550 // Check for repeater fields using the old method
1551 checkRepeaterFields(settings, textFields);
1552 }
1553
1554 return textFields;
1555 }
1556
1557 /**
1558 * Get widget controls schema from Elementor
1559 */
1560 function getWidgetControls(widgetType, container) {
1561 try {
1562 // Method 1: Try to get controls from container model
1563 if (container && container.model && container.model.get) {
1564 var model = container.model;
1565
1566 // Try to get controls from the model's widget config
1567 if (model.config && model.config.controls) {
1568 return model.config.controls;
1569 }
1570
1571 // Try to get controls from the container settings
1572 if (container.settings && container.settings.controls) {
1573 return container.settings.controls;
1574 }
1575 }
1576
1577 // Method 2: Try to get controls from Elementor widgets registry
1578 if (window.elementor && elementor.widgets) {
1579 var widgetConfig = elementor.widgets.getWidgetType(widgetType);
1580 if (widgetConfig && widgetConfig.controls) {
1581 return widgetConfig.controls;
1582 }
1583 }
1584
1585 // Method 3: Try to get controls from elements manager
1586 if (window.elementor && elementor.elementsManager) {
1587 var elementView = elementor.elementsManager.getElementView(container.model.get('id'));
1588 if (elementView && elementView.model && elementView.model.controls) {
1589 return elementView.model.controls;
1590 }
1591 }
1592
1593 return null;
1594
1595 } catch (error) {
1596 console.warn('⚠️ Error getting widget controls:', error);
1597 return null;
1598 }
1599 }
1600
1601 /**
1602 * Check repeater fields using control type information
1603 */
1604 function checkRepeaterFieldsByType(repeaterName, repeaterControl, repeaterData, textFields) {
1605 try {
1606 // Get the fields schema for this repeater
1607 var repeaterFields = repeaterControl.fields || repeaterControl.controls || {};
1608
1609 // Find text-based fields in the repeater schema
1610 var textFieldNames = [];
1611 Object.keys(repeaterFields).forEach(function(fieldName) {
1612 var fieldControl = repeaterFields[fieldName];
1613 var textControlTypes = ['text', 'textarea', 'wysiwyg', 'url', 'email'];
1614
1615 if (textControlTypes.includes(fieldControl.type)) {
1616 textFieldNames.push(fieldName);
1617 }
1618 });
1619
1620 if (textFieldNames.length === 0) {
1621 return;
1622 }
1623
1624 // Process repeater data (same as before)
1625 if (repeaterData && typeof repeaterData === 'object' && repeaterData.models) {
1626 // Backbone collection
1627 const models = repeaterData.models || [];
1628 for (let i = 0; i < models.length; i++) {
1629 const model = models[i];
1630 const modelData = model.attributes || model.toJSON();
1631
1632 for (const fieldName of textFieldNames) {
1633 if (modelData[fieldName] && typeof modelData[fieldName] === 'string' && modelData[fieldName].trim()) {
1634 const fieldKey = `${repeaterName}[${i}][${fieldName}]`;
1635 const fieldValue = modelData[fieldName];
1636
1637 textFields.push({
1638 field: fieldKey,
1639 value: fieldValue,
1640 type: 'text',
1641 isRepeater: true,
1642 repeaterKey: repeaterName,
1643 repeaterIndex: i,
1644 repeaterField: fieldName
1645 });
1646 }
1647 }
1648 }
1649 } else if (Array.isArray(repeaterData)) {
1650 // Regular array
1651 for (let i = 0; i < repeaterData.length; i++) {
1652 const item = repeaterData[i];
1653 for (const fieldName of textFieldNames) {
1654 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
1655 const fieldKey = `${repeaterName}[${i}][${fieldName}]`;
1656 const fieldValue = item[fieldName];
1657
1658 textFields.push({
1659 field: fieldKey,
1660 value: fieldValue,
1661 type: 'text',
1662 isRepeater: true,
1663 repeaterKey: repeaterName,
1664 repeaterIndex: i,
1665 repeaterField: fieldName
1666 });
1667 }
1668 }
1669 }
1670 }
1671
1672 } catch (error) {
1673 console.warn('⚠️ Error processing repeater by type:', error);
1674 // Fallback to old method
1675 var repeaterConfig = {};
1676 repeaterConfig[repeaterName] = ['content', 'text', 'title', 'description'];
1677 checkRepeaterFields({[repeaterName]: repeaterData}, textFields);
1678 }
1679 }
1680
1681 /**
1682 * Check for repeater fields in settings (fallback method)
1683 */
1684 function checkRepeaterFields(settings, textFields) {
1685 // King Addons specific repeater configurations
1686 const repeaterConfigs = {
1687 'kng_styled_txt_content_items': ['kng_styled_txt_content'],
1688 'kng_tabs_items': ['kng_tabs_title', 'kng_tabs_content'],
1689 'kng_accordion_items': ['kng_accordion_title', 'kng_accordion_content'],
1690 'kng_testimonials_items': ['kng_testimonials_content', 'kng_testimonials_name'],
1691 'kng_team_members': ['kng_team_name', 'kng_team_position', 'kng_team_description'],
1692 'kng_price_list_items': ['kng_price_title', 'kng_price_description'],
1693 'kng_business_hours_items': ['kng_business_day', 'kng_business_hours'],
1694 // Standard Elementor repeaters
1695 'tabs': ['tab_title', 'tab_content'],
1696 'icon_list': ['text'],
1697 'slides': ['heading', 'description', 'button_text'],
1698 'list_items': ['text'],
1699 'testimonials': ['testimonial_content', 'testimonial_name'],
1700 'items': ['item_title', 'item_description', 'item_content'],
1701 'price_list': ['price_title', 'price_description']
1702 };
1703
1704 for (const [repeaterKey, fieldNames] of Object.entries(repeaterConfigs)) {
1705 if (settings[repeaterKey]) {
1706 let repeaterData = settings[repeaterKey];
1707
1708 // Handle Backbone Collections (common in King Addons and some Elementor widgets)
1709 if (repeaterData && typeof repeaterData === 'object' && repeaterData.models) {
1710 // Extract models from Backbone collection
1711 const models = repeaterData.models || [];
1712 for (let i = 0; i < models.length; i++) {
1713 const model = models[i];
1714 const modelData = model.attributes || model.toJSON();
1715
1716 for (const fieldName of fieldNames) {
1717 if (modelData[fieldName] && typeof modelData[fieldName] === 'string' && modelData[fieldName].trim()) {
1718 const fieldKey = `${repeaterKey}[${i}][${fieldName}]`;
1719 const fieldValue = modelData[fieldName];
1720
1721 textFields.push({
1722 field: fieldKey,
1723 value: fieldValue,
1724 type: 'text',
1725 isRepeater: true,
1726 repeaterKey: repeaterKey,
1727 repeaterIndex: i,
1728 repeaterField: fieldName
1729 });
1730 }
1731 }
1732 }
1733 }
1734 // Handle regular arrays
1735 else if (Array.isArray(repeaterData)) {
1736 for (let i = 0; i < repeaterData.length; i++) {
1737 const item = repeaterData[i];
1738 for (const fieldName of fieldNames) {
1739 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
1740 const fieldKey = `${repeaterKey}[${i}][${fieldName}]`;
1741 const fieldValue = item[fieldName];
1742
1743 textFields.push({
1744 field: fieldKey,
1745 value: fieldValue,
1746 type: 'text',
1747 isRepeater: true,
1748 repeaterKey: repeaterKey,
1749 repeaterIndex: i,
1750 repeaterField: fieldName
1751 });
1752 }
1753 }
1754 }
1755 }
1756 // Handle objects with numbered keys (alternative format)
1757 else if (repeaterData && typeof repeaterData === 'object') {
1758 const keys = Object.keys(repeaterData).filter(key => /^\d+$/.test(key));
1759
1760 for (const key of keys) {
1761 const item = repeaterData[key];
1762 for (const fieldName of fieldNames) {
1763 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
1764 const fieldKey = `${repeaterKey}[${key}][${fieldName}]`;
1765 const fieldValue = item[fieldName];
1766
1767 textFields.push({
1768 field: fieldKey,
1769 value: fieldValue,
1770 type: 'text',
1771 isRepeater: true,
1772 repeaterKey: repeaterKey,
1773 repeaterIndex: key,
1774 repeaterField: fieldName
1775 });
1776 }
1777 }
1778 }
1779 }
1780 }
1781 }
1782 }
1783
1784 /**
1785 * Show progress UI in popup
1786 */
1787 function showProgressInPopup($popup) {
1788 // Update header for compact mode with close button
1789 var headerHtml = `
1790 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;"/>
1791 AI Translation in Progress
1792 <button class="king-addons-translator-close-btn" title="Close">×</button>
1793 `;
1794
1795 var progressHtml = `
1796 <div class="king-addons-translator-progress">
1797 <div class="king-addons-translator-progress-text">
1798 Translating page elements... <span id="king-addons-progress-count">0 / ${translationState.totalElements}</span>
1799 </div>
1800 <div class="king-addons-translator-progress-bar">
1801 <div class="king-addons-translator-progress-fill" id="king-addons-progress-fill"></div>
1802 </div>
1803 <div class="king-addons-translator-current-element" id="king-addons-current-element">
1804 Starting translation...
1805 </div>
1806 </div>
1807 `;
1808
1809 // Update header
1810 $popup.find('h3').html(headerHtml);
1811
1812 // Hide the subtitle/description text during translation
1813 $popup.find('h3').next('div').hide();
1814
1815 $popup.find('.king-addons-translator-form').html(progressHtml);
1816
1817 // Note: Close button events are handled by global document handler to prevent duplicates
1818 }
1819
1820 /**
1821 * Translate elements sequentially
1822 */
1823 function translateElementsSequentially(elements, index, $popup) {
1824 // Check if translation was cancelled
1825 if (translationState.isCancelled) {
1826 return;
1827 }
1828
1829 if (index >= elements.length) {
1830 showTranslationComplete($popup);
1831 return;
1832 }
1833
1834 var element = elements[index];
1835 translationState.currentElement = element;
1836
1837 // Update progress UI
1838 updateProgressUI(index + 1, element);
1839
1840 // Highlight current element in preview
1841 highlightElementInPreview(element.elementId, true);
1842
1843 // Translate all text fields for this element
1844 translateElementFields(element, function(success) {
1845 // Remove highlight
1846 highlightElementInPreview(element.elementId, false);
1847
1848 if (success) {
1849 translationState.translatedElements++;
1850 showElementSuccess(element.elementId);
1851 } else {
1852 translationState.failedElements++;
1853 }
1854
1855 // Continue with next element after a short delay
1856 setTimeout(function() {
1857 // Check if translation was cancelled before proceeding
1858 if (!translationState.isCancelled) {
1859 translateElementsSequentially(elements, index + 1, $popup);
1860 }
1861 }, 500);
1862 });
1863 }
1864
1865 /**
1866 * Translate all text fields for an element
1867 */
1868 function translateElementFields(element, callback) {
1869 // Check if translation was cancelled before starting
1870 if (translationState.isCancelled) {
1871 callback(false);
1872 return;
1873 }
1874
1875 var fieldsToTranslate = element.textFields.slice();
1876 var translatedFields = {};
1877 var completedFields = 0;
1878 var hasErrors = false;
1879
1880 if (fieldsToTranslate.length === 0) {
1881 callback(true);
1882 return;
1883 }
1884
1885 function translateNextField() {
1886 // Check if translation was cancelled before processing next field
1887 if (translationState.isCancelled) {
1888 callback(false);
1889 return;
1890 }
1891
1892 if (completedFields >= fieldsToTranslate.length) {
1893 // All fields translated, update the element
1894 if (Object.keys(translatedFields).length > 0 && !translationState.isCancelled) {
1895 updateElementSettings(element.container, translatedFields);
1896 }
1897 callback(!hasErrors);
1898 return;
1899 }
1900
1901 var field = fieldsToTranslate[completedFields];
1902 translateSingleField(field.value, function(translatedText, success) {
1903 // Check if translation was cancelled while waiting for response
1904 if (translationState.isCancelled) {
1905 callback(false);
1906 return;
1907 }
1908
1909 if (success && translatedText) {
1910 translatedFields[field.field] = translatedText;
1911 } else {
1912 hasErrors = true;
1913 }
1914
1915 completedFields++;
1916 setTimeout(translateNextField, 200); // Small delay between field translations
1917 });
1918 }
1919
1920 translateNextField();
1921 }
1922
1923 /**
1924 * Translate a single text field
1925 */
1926 function translateSingleField(text, callback) {
1927 // Check if translation was cancelled before making request
1928 if (translationState.isCancelled) {
1929 callback(text, false);
1930 return;
1931 }
1932
1933 var request = $.post(KingAddonsAiField.ajax_url, {
1934 action: 'king_addons_ai_translate_text',
1935 nonce: KingAddonsAiField.generate_nonce,
1936 text: text,
1937 from_lang: translationState.fromLang,
1938 to_lang: translationState.toLang
1939 }, function(response) {
1940 // Remove request from active requests list
1941 var index = translationState.currentRequests.indexOf(request);
1942 if (index > -1) {
1943 translationState.currentRequests.splice(index, 1);
1944 }
1945
1946 // Check if translation was cancelled while request was in progress
1947 if (translationState.isCancelled) {
1948 callback(text, false);
1949 return;
1950 }
1951
1952 if (response.success && response.data.translated_text) {
1953 callback(response.data.translated_text, true);
1954 } else {
1955 // Handle API errors with more detail
1956 var errorMessage = 'Translation failed';
1957 if (response.data && response.data.message) {
1958 errorMessage = response.data.message;
1959 } else if (!response.success && response.data) {
1960 errorMessage = typeof response.data === 'string' ? response.data : 'API Error';
1961 }
1962
1963 console.error('Translation API Error:', errorMessage);
1964
1965 // Check for token limit errors first
1966 if (errorMessage.toLowerCase().includes('token limit') ||
1967 errorMessage.toLowerCase().includes('daily limit') ||
1968 errorMessage.toLowerCase().includes('limit reached') ||
1969 errorMessage.toLowerCase().includes('quota exceeded') ||
1970 errorMessage.toLowerCase().includes('rate limit') ||
1971 errorMessage.toLowerCase().includes('too many requests')) {
1972
1973 // Stop translation process for limit errors
1974 stopTranslationProcess();
1975
1976 // Show token limit error popup
1977 setTimeout(function() {
1978 showTokenLimitError(errorMessage);
1979 }, 500);
1980 return;
1981 }
1982
1983 // Show error notification for API key issues
1984 if (errorMessage.toLowerCase().includes('api key') ||
1985 errorMessage.toLowerCase().includes('invalid') ||
1986 errorMessage.toLowerCase().includes('unauthorized')) {
1987
1988 // Stop translation process for critical errors
1989 stopTranslationProcess();
1990
1991 // Show API key error popup
1992 setTimeout(function() {
1993 showApiKeyError('Setup Required', errorMessage + '\n\nPlease check your API key in settings and try again.');
1994 }, 500);
1995 return;
1996 }
1997
1998 callback(text, false);
1999 }
2000 }).fail(function(xhr, textStatus, errorThrown) {
2001 // Remove request from active requests list
2002 var index = translationState.currentRequests.indexOf(request);
2003 if (index > -1) {
2004 translationState.currentRequests.splice(index, 1);
2005 }
2006
2007 // Don't process failed requests if cancelled
2008 if (translationState.isCancelled) {
2009 return;
2010 }
2011
2012 // Handle different types of network errors
2013 var errorMessage = 'Network error occurred';
2014 var shouldShowError = false;
2015
2016 if (xhr.status === 0) {
2017 errorMessage = 'Network connection failed. Please check your internet connection.';
2018 shouldShowError = true;
2019 } else if (xhr.status === 401) {
2020 errorMessage = 'API key is invalid or expired. Please check your API key in settings.';
2021 shouldShowError = true;
2022 } else if (xhr.status === 403) {
2023 errorMessage = 'Access forbidden. Please check your API key permissions.';
2024 shouldShowError = true;
2025 } else if (xhr.status === 429) {
2026 // Try to get detailed error message from response
2027 var detailedMessage = 'Rate limit exceeded. Please wait a moment and try again.';
2028 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
2029 detailedMessage = xhr.responseJSON.data.message;
2030 } else if (xhr.responseText) {
2031 try {
2032 var response = JSON.parse(xhr.responseText);
2033 if (response && response.data && response.data.message) {
2034 detailedMessage = response.data.message;
2035 }
2036 } catch (e) {
2037 // Keep default message if parsing fails
2038 }
2039 }
2040 errorMessage = detailedMessage;
2041 shouldShowError = true;
2042 } else if (xhr.status >= 500) {
2043 errorMessage = 'Server error. Please try again later.';
2044 shouldShowError = true;
2045 }
2046
2047 console.error('Translation Network Error:', {
2048 status: xhr.status,
2049 statusText: textStatus,
2050 error: errorThrown,
2051 message: errorMessage,
2052 responseText: xhr.responseText,
2053 responseJSON: xhr.responseJSON
2054 });
2055
2056 // Show error popup for critical network issues
2057 if (shouldShowError) {
2058 stopTranslationProcess();
2059
2060 setTimeout(function() {
2061 // Check for token limit errors first (including 429 status)
2062 if (errorMessage.toLowerCase().includes('token limit') ||
2063 errorMessage.toLowerCase().includes('daily limit') ||
2064 errorMessage.toLowerCase().includes('limit reached') ||
2065 errorMessage.toLowerCase().includes('quota exceeded') ||
2066 errorMessage.toLowerCase().includes('rate limit exceeded') ||
2067 errorMessage.toLowerCase().includes('too many requests') ||
2068 (xhr.status === 429 && (
2069 errorMessage.toLowerCase().includes('limit') ||
2070 errorMessage.toLowerCase().includes('exceeded') ||
2071 errorMessage.toLowerCase().includes('quota')
2072 ))) {
2073
2074 showTokenLimitError(errorMessage);
2075 return;
2076 }
2077
2078 if (xhr.status === 401 || xhr.status === 403) {
2079 showApiKeyError('Authentication Error', errorMessage);
2080 } else {
2081 showApiKeyError('Network Error', errorMessage);
2082 }
2083 }, 500);
2084 return;
2085 }
2086
2087 callback(text, false);
2088 });
2089
2090 // Store the request so we can cancel it if needed
2091 translationState.currentRequests.push(request);
2092 }
2093
2094 /**
2095 * Update element settings with translated text
2096 */
2097 function updateElementSettings(container, translatedFields) {
2098 try {
2099 // Separate regular fields from repeater fields
2100 var regularFields = {};
2101 var repeaterUpdates = {};
2102
2103 Object.keys(translatedFields).forEach(function(fieldKey) {
2104 var translatedValue = translatedFields[fieldKey];
2105
2106 // Check if this is a repeater field
2107 var repeaterMatch = fieldKey.match(/^(.+)\[(\d+)\]\[(.+)\]$/);
2108 if (repeaterMatch) {
2109 // This is a repeater field: repeaterKey[index][fieldName]
2110 var repeaterKey = repeaterMatch[1];
2111 var itemIndex = parseInt(repeaterMatch[2]);
2112 var itemField = repeaterMatch[3];
2113
2114 if (!repeaterUpdates[repeaterKey]) {
2115 repeaterUpdates[repeaterKey] = {};
2116 }
2117 if (!repeaterUpdates[repeaterKey][itemIndex]) {
2118 repeaterUpdates[repeaterKey][itemIndex] = {};
2119 }
2120 repeaterUpdates[repeaterKey][itemIndex][itemField] = translatedValue;
2121 } else {
2122 // Regular field
2123 regularFields[fieldKey] = translatedValue;
2124 }
2125 });
2126
2127 // Apply regular field updates
2128 if (Object.keys(regularFields).length > 0) {
2129 $e.run('document/elements/settings', {
2130 container: container,
2131 settings: regularFields
2132 });
2133 }
2134
2135 // Apply repeater field updates
2136 Object.keys(repeaterUpdates).forEach(function(repeaterKey) {
2137 var currentSettings = container.settings.get(repeaterKey);
2138
2139 // Handle Backbone Collections (King Addons and some Elementor widgets)
2140 if (currentSettings && typeof currentSettings.models !== 'undefined') {
2141 // Work with Backbone collection
2142 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2143 var index = parseInt(itemIndex);
2144 if (currentSettings.models[index]) {
2145 var model = currentSettings.models[index];
2146
2147 // Update the specific fields in this repeater item
2148 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2149 var oldValue = model.get(fieldName);
2150 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2151
2152 // Update the model attribute
2153 model.set(fieldName, newValue);
2154 });
2155 }
2156 });
2157
2158 // Use Elementor's proper API to notify of changes instead of direct trigger
2159 try {
2160 // Method 1: Use Elementor's run command to update the entire repeater
2161 var backboneData = currentSettings.toJSON ? currentSettings.toJSON() :
2162 currentSettings.models.map(function(model) {
2163 return model.toJSON ? model.toJSON() : model.attributes;
2164 });
2165
2166 var repeaterSettings = {};
2167 repeaterSettings[repeaterKey] = backboneData;
2168
2169 $e.run('document/elements/settings', {
2170 container: container,
2171 settings: repeaterSettings
2172 });
2173 } catch (e) {
2174 console.warn('⚠️ Error updating via Elementor API, trying alternative method:', e);
2175
2176 // Fallback: Try to manually trigger save without change events
2177 try {
2178 if (typeof container.saveSettings === 'function') {
2179 container.saveSettings();
2180 }
2181 } catch (e2) {
2182 console.warn('⚠️ Fallback method also failed:', e2);
2183 }
2184 }
2185 }
2186 // Handle regular arrays (standard Elementor repeaters)
2187 else if (Array.isArray(currentSettings)) {
2188 var updatedRepeater = currentSettings.slice(); // Clone array
2189
2190 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2191 var index = parseInt(itemIndex);
2192 if (updatedRepeater[index]) {
2193 // Update the specific fields in this repeater item
2194 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2195 var oldValue = updatedRepeater[index][fieldName];
2196 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2197 updatedRepeater[index][fieldName] = newValue;
2198 });
2199 }
2200 });
2201
2202 // Update the entire repeater field
2203 var repeaterSettings = {};
2204 repeaterSettings[repeaterKey] = updatedRepeater;
2205
2206 $e.run('document/elements/settings', {
2207 container: container,
2208 settings: repeaterSettings
2209 });
2210 }
2211 // Handle objects with numbered keys
2212 else if (currentSettings && typeof currentSettings === 'object') {
2213 var updatedObject = Object.assign({}, currentSettings); // Clone object
2214
2215 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2216 if (updatedObject[itemIndex]) {
2217 // Update the specific fields in this repeater item
2218 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2219 var oldValue = updatedObject[itemIndex][fieldName];
2220 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2221 updatedObject[itemIndex][fieldName] = newValue;
2222 });
2223 }
2224 });
2225
2226 // Update the entire repeater field
2227 var repeaterSettings = {};
2228 repeaterSettings[repeaterKey] = updatedObject;
2229
2230 $e.run('document/elements/settings', {
2231 container: container,
2232 settings: repeaterSettings
2233 });
2234 } else {
2235 }
2236 });
2237
2238 } catch (error) {
2239 console.error('❌ Error updating element settings:', error);
2240 console.error('Error details:', {
2241 message: error.message,
2242 stack: error.stack,
2243 translatedFields: translatedFields,
2244 widgetType: container.model.get('widgetType')
2245 });
2246 }
2247 }
2248
2249 /**
2250 * Update progress UI
2251 */
2252 function updateProgressUI(current, element) {
2253 var percentage = (current / translationState.totalElements) * 100;
2254
2255 $('#king-addons-progress-count').text(current + ' / ' + translationState.totalElements);
2256 $('#king-addons-progress-fill').css('width', percentage + '%');
2257 $('#king-addons-current-element').text('Translating: ' + element.widgetType + ' (' + element.textFields.length + ' fields)');
2258 }
2259
2260 /**
2261 * Inject animation styles into preview iframe
2262 */
2263 function injectPreviewStyles() {
2264 if (!elementor || !elementor.$preview) return;
2265
2266 var $previewDoc = elementor.$preview.contents();
2267 var $previewHead = $previewDoc.find('head');
2268
2269 if ($previewHead.length && !$previewDoc.find('#king-addons-preview-translator-styles').length) {
2270 var previewStyles = `
2271 <style id="king-addons-preview-translator-styles">
2272 /* Element highlighting animation for translation */
2273 .king-addons-translating-element {
2274 position: relative !important;
2275 border: 3px solid #2196F3 !important;
2276 box-shadow: 0 0 20px rgba(33, 150, 243, 0.4) !important;
2277 border-radius: 4px !important;
2278 animation: king-addons-translate-pulse 1.5s infinite ease-in-out !important;
2279 z-index: 999 !important;
2280 }
2281
2282 .king-addons-translating-element::before {
2283 content: "🔄 Translating..." !important;
2284 position: absolute !important;
2285 top: -35px !important;
2286 left: 50% !important;
2287 transform: translateX(-50%) !important;
2288 background: #2196F3 !important;
2289 color: white !important;
2290 padding: 6px 12px !important;
2291 border-radius: 20px !important;
2292 font-size: 12px !important;
2293 font-weight: 600 !important;
2294 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
2295 z-index: 10000 !important;
2296 animation: king-addons-translate-bounce 0.8s ease-out !important;
2297 box-shadow: 0 3px 10px rgba(33, 150, 243, 0.3) !important;
2298 white-space: nowrap !important;
2299 }
2300
2301 .king-addons-translated-element {
2302 position: relative !important;
2303 border: 3px solid #4CAF50 !important;
2304 box-shadow: 0 0 20px rgba(76, 175, 80, 0.4) !important;
2305 border-radius: 4px !important;
2306 animation: king-addons-translate-success 1.2s ease-out !important;
2307 z-index: 999 !important;
2308 }
2309
2310 .king-addons-translated-element::before {
2311 content: "�
2312 Translated!" !important;
2313 position: absolute !important;
2314 top: -35px !important;
2315 left: 50% !important;
2316 transform: translateX(-50%) !important;
2317 background: #4CAF50 !important;
2318 color: white !important;
2319 padding: 6px 12px !important;
2320 border-radius: 20px !important;
2321 font-size: 12px !important;
2322 font-weight: 600 !important;
2323 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
2324 z-index: 10000 !important;
2325 animation: king-addons-translate-bounce 0.8s ease-out !important;
2326 box-shadow: 0 3px 10px rgba(76, 175, 80, 0.3) !important;
2327 white-space: nowrap !important;
2328 }
2329
2330 /* Pulsing animation for translating elements */
2331 @keyframes king-addons-translate-pulse {
2332 0% {
2333 box-shadow: 0 0 0 0 rgba(33, 150, 243, 0.7),
2334 0 0 20px rgba(33, 150, 243, 0.4);
2335 transform: scale(1);
2336 }
2337 50% {
2338 box-shadow: 0 0 0 8px rgba(33, 150, 243, 0.2),
2339 0 0 30px rgba(33, 150, 243, 0.6);
2340 transform: scale(1.02);
2341 }
2342 100% {
2343 box-shadow: 0 0 0 0 rgba(33, 150, 243, 0),
2344 0 0 20px rgba(33, 150, 243, 0.4);
2345 transform: scale(1);
2346 }
2347 }
2348
2349 /* Success animation for completed elements */
2350 @keyframes king-addons-translate-success {
2351 0% {
2352 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7),
2353 0 0 20px rgba(76, 175, 80, 0.4);
2354 transform: scale(1);
2355 }
2356 20% {
2357 box-shadow: 0 0 0 12px rgba(76, 175, 80, 0.3),
2358 0 0 40px rgba(76, 175, 80, 0.6);
2359 transform: scale(1.05);
2360 }
2361 40% {
2362 transform: scale(0.98);
2363 }
2364 60% {
2365 transform: scale(1.02);
2366 }
2367 80% {
2368 transform: scale(0.99);
2369 }
2370 100% {
2371 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0),
2372 0 0 20px rgba(76, 175, 80, 0.2);
2373 transform: scale(1);
2374 }
2375 }
2376
2377 /* Bounce animation for labels */
2378 @keyframes king-addons-translate-bounce {
2379 0% {
2380 transform: translateX(-50%) translateY(-10px) scale(0.8);
2381 opacity: 0;
2382 }
2383 50% {
2384 transform: translateX(-50%) translateY(-2px) scale(1.1);
2385 opacity: 1;
2386 }
2387 70% {
2388 transform: translateX(-50%) translateY(-1px) scale(0.95);
2389 }
2390 100% {
2391 transform: translateX(-50%) translateY(0) scale(1);
2392 opacity: 1;
2393 }
2394 }
2395 </style>
2396 `;
2397 $previewHead.append(previewStyles);
2398 }
2399 }
2400
2401 /**
2402 * Highlight element in preview
2403 */
2404 function highlightElementInPreview(elementId, highlight) {
2405 // Ensure preview styles are injected
2406 injectPreviewStyles();
2407
2408 // Find element in preview iframe
2409 if (!elementor || !elementor.$preview) {
2410 return;
2411 }
2412
2413 var $previewDoc = elementor.$preview.contents();
2414 var $previewElement = $previewDoc.find('[data-id="' + elementId + '"]');
2415
2416 if ($previewElement.length === 0) {
2417 return;
2418 }
2419
2420 if (highlight) {
2421 // Remove any existing classes first
2422 $previewElement.removeClass('king-addons-translated-element');
2423 $previewElement.addClass('king-addons-translating-element');
2424 } else {
2425 $previewElement.removeClass('king-addons-translating-element');
2426 }
2427 }
2428
2429 /**
2430 * Show element success animation
2431 */
2432 function showElementSuccess(elementId) {
2433 // Ensure preview styles are injected
2434 injectPreviewStyles();
2435
2436 // Find element in preview iframe
2437 if (!elementor || !elementor.$preview) {
2438 return;
2439 }
2440
2441 var $previewDoc = elementor.$preview.contents();
2442 var $previewElement = $previewDoc.find('[data-id="' + elementId + '"]');
2443
2444 if ($previewElement.length === 0) {
2445 return;
2446 }
2447
2448 // Remove translating class and add translated class
2449 $previewElement.removeClass('king-addons-translating-element');
2450 $previewElement.addClass('king-addons-translated-element');
2451
2452 // Remove the success animation after it completes
2453 setTimeout(function() {
2454 $previewElement.removeClass('king-addons-translated-element');
2455 }, 1200);
2456 }
2457
2458 /**
2459 * Show translation complete with stats (stays open until manually closed)
2460 */
2461 function showTranslationComplete($popup) {
2462 if (!$popup || $popup.length === 0) {
2463 console.error('❌ Cannot show translation results: popup not found');
2464 return;
2465 }
2466
2467 var statsHtml = `
2468 <div class="king-addons-translator-progress">
2469 <div class="king-addons-translator-progress-text">
2470 �
2471 Translation Complete!
2472 </div>
2473 <div class="king-addons-translator-stats">
2474 <div class="king-addons-translator-stat">
2475 <div class="king-addons-translator-stat-number" style="font-size: 32px; font-weight: bold; color: #2196F3;">${translationState.totalElements}</div>
2476 <div class="king-addons-translator-stat-label">Total Elements</div>
2477 </div>
2478 <div class="king-addons-translator-stat">
2479 <div class="king-addons-translator-stat-number" style="font-size: 32px; font-weight: bold; color: #4CAF50; animation: pulse 2s infinite;">${translationState.translatedElements}</div>
2480 <div class="king-addons-translator-stat-label">�
2481 Translated</div>
2482 </div>
2483 <div class="king-addons-translator-stat">
2484 <div class="king-addons-translator-stat-number" style="font-size: 32px; font-weight: bold; color: ${translationState.failedElements > 0 ? '#F44336' : '#999'};">${translationState.failedElements}</div>
2485 <div class="king-addons-translator-stat-label">${translationState.failedElements > 0 ? '❌' : '⚪'} Failed</div>
2486 </div>
2487 </div>
2488 <div class="king-addons-translator-actions" style="margin-top: 20px; text-align: center;">
2489 <button class="king-addons-translator-btn-primary" id="king-addons-close-stats" style="background: #4CAF50; color: white; border: none; padding: 12px 30px; font-size: 16px; font-weight: bold; border-radius: 6px; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); min-width: 120px;">
2490 Close Results
2491 </button>
2492 </div>
2493 </div>
2494 `;
2495
2496 var $form = $popup.find('.king-addons-translator-form');
2497
2498 $form.html(statsHtml);
2499
2500 // Play success sound (Web Audio API)
2501 try {
2502 var audioContext = new (window.AudioContext || window.webkitAudioContext)();
2503 var oscillator = audioContext.createOscillator();
2504 var gainNode = audioContext.createGain();
2505
2506 oscillator.connect(gainNode);
2507 gainNode.connect(audioContext.destination);
2508
2509 oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
2510 oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1);
2511 oscillator.frequency.setValueAtTime(1200, audioContext.currentTime + 0.2);
2512
2513 gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
2514 gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
2515
2516 oscillator.start(audioContext.currentTime);
2517 oscillator.stop(audioContext.currentTime + 0.3);
2518 } catch (e) {
2519 }
2520
2521 // Show temporary notification to attract attention
2522 var $notificationBanner = $('<div style="position: fixed; top: 0; left: 0; right: 0; background: linear-gradient(135deg, #4CAF50, #2E7D32); color: white; padding: 12px; text-align: center; font-size: 16px; font-weight: bold; z-index: 1000000; box-shadow: 0 2px 10px rgba(0,0,0,0.3); animation: slideDown 0.5s ease;">🎉 Translation Complete! Check the results popup below.</div>');
2523 $('body').append($notificationBanner);
2524
2525 // Remove notification after 5 seconds
2526 setTimeout(function() {
2527 $notificationBanner.fadeOut(300, function() {
2528 $notificationBanner.remove();
2529 });
2530 }, 5000);
2531
2532 // Update header to show completion
2533 var completionHeaderHtml = `
2534 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);"/>
2535 AI Translation Complete
2536 `;
2537 $popup.find('h3').html(completionHeaderHtml);
2538
2539 // Show the subtitle/description text again
2540 $popup.find('h3').next('div').show();
2541
2542 // If popup is in compact mode, move it back to center for better visibility
2543 if ($popup.hasClass('compact')) {
2544 // Remove compact class and positioning
2545 $popup.removeClass('compact moving');
2546 $popup.css({
2547 'position': 'fixed',
2548 'top': '50%',
2549 'left': '50%',
2550 'transform': 'translate(-50%, -50%)',
2551 'right': 'auto',
2552 'bottom': 'auto',
2553 'width': '500px',
2554 'max-width': '90vw',
2555 'z-index': '999999',
2556 'background': 'white',
2557 'border-radius': '8px',
2558 'box-shadow': '0 10px 25px rgba(0,0,0,0.2)',
2559 'opacity': '1',
2560 'visibility': 'visible'
2561 });
2562
2563 // Re-add overlay if it doesn't exist
2564 if (!$popup.closest('.king-addons-translator-overlay').length) {
2565 var $overlay = $('<div class="king-addons-translator-overlay"></div>').css({
2566 'position': 'fixed',
2567 'top': '0',
2568 'left': '0',
2569 'width': '100%',
2570 'height': '100%',
2571 'background': 'rgba(0, 0, 0, 0.5)',
2572 'z-index': '999998',
2573 'display': 'flex',
2574 'align-items': 'center',
2575 'justify-content': 'center'
2576 });
2577 $popup.wrap($overlay);
2578 } else {
2579 // Make sure existing overlay is visible
2580 $popup.closest('.king-addons-translator-overlay').css({
2581 'z-index': '999998',
2582 'display': 'flex'
2583 });
2584 }
2585
2586 // Add entrance animation
2587 $popup.css('opacity', '0').animate({'opacity': '1'}, 300);
2588 } else {
2589 // For non-compact popups, ensure they're also properly visible
2590 $popup.css({
2591 'z-index': '999999',
2592 'opacity': '1',
2593 'visibility': 'visible',
2594 'position': 'fixed'
2595 });
2596
2597 // Make sure overlay is visible
2598 var $overlay = $popup.closest('.king-addons-translator-overlay');
2599 if ($overlay.length) {
2600 $overlay.css({
2601 'z-index': '999998',
2602 'display': 'block',
2603 'opacity': '1',
2604 'visibility': 'visible'
2605 });
2606 }
2607
2608 // Add entrance animation
2609 $popup.css('opacity', '0').animate({'opacity': '1'}, 300);
2610 }
2611
2612 $('#king-addons-close-stats').on('click', function() {
2613 // For compact popup, just remove it directly since overlay is already gone
2614 if ($popup.hasClass('compact')) {
2615 $popup.remove();
2616 } else {
2617 $popup.closest('.king-addons-translator-overlay').remove();
2618 }
2619 });
2620
2621 // Reset translation state and re-enable button
2622 translationState.isTranslating = false;
2623 translationState.isCancelled = false;
2624 translationState.currentRequests = []; // Clear any remaining requests
2625 toggleTranslatorButton(false);
2626
2627 // Note: Auto-close removed by user request - popup stays open until manually closed
2628 }
2629
2630 /**
2631 * Handle Elementor initialization
2632 */
2633 function onElementorInit() {
2634 // Check if AI Page Translator is enabled
2635 if (typeof KingAddonsAiField !== 'undefined' && KingAddonsAiField.translator_enabled === false) {
2636 return;
2637 }
2638
2639 // Inject styles first
2640 injectTranslatorStyles();
2641
2642 // Try to inject preview styles (will work when preview is available)
2643 setTimeout(function() {
2644 injectPreviewStyles();
2645 }, 1000);
2646
2647 // Add button immediately
2648 addTranslatorButton();
2649
2650 // Also add button when panel opens
2651 if (typeof elementor !== 'undefined' && elementor.hooks) {
2652 elementor.hooks.addAction('panel/open_editor/widget', function() {
2653 setTimeout(addTranslatorButton, 100);
2654 });
2655
2656 // Add button when navigator opens
2657 elementor.hooks.addAction('navigator/init', function() {
2658 setTimeout(addTranslatorButton, 100);
2659 });
2660
2661 // Inject preview styles when preview loads
2662 elementor.hooks.addAction('preview/loaded', function() {
2663 injectPreviewStyles();
2664 });
2665 }
2666
2667 // Monitor for panel changes
2668 observePanelChanges();
2669 }
2670
2671 /**
2672 * Observe panel changes to re-add button if needed
2673 */
2674 function observePanelChanges() {
2675 function createObserver() {
2676 return new MutationObserver(function(mutations) {
2677 var shouldCheck = false;
2678
2679 mutations.forEach(function(mutation) {
2680 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
2681 shouldCheck = true;
2682 }
2683 });
2684
2685 if (shouldCheck) {
2686 setTimeout(addTranslatorButton, 300);
2687 }
2688 });
2689 }
2690
2691 // Observe changes in the top toolbar (priority)
2692 var topToolbar = document.querySelector('#elementor-editor-wrapper-v2 .MuiToolbar-root');
2693 if (topToolbar) {
2694 var topObserver = createObserver();
2695 topObserver.observe(topToolbar, {
2696 childList: true,
2697 subtree: true
2698 });
2699 }
2700
2701 // Also observe the main editor wrapper for structural changes
2702 var editorWrapper = document.querySelector('#elementor-editor-wrapper-v2');
2703 if (editorWrapper) {
2704 var wrapperObserver = createObserver();
2705 wrapperObserver.observe(editorWrapper, {
2706 childList: true,
2707 subtree: false
2708 });
2709 }
2710
2711 // Observe changes in the main panel (fallback)
2712 var panel = document.querySelector('#elementor-panel');
2713 if (panel) {
2714 var panelObserver = createObserver();
2715 panelObserver.observe(panel, {
2716 childList: true,
2717 subtree: true
2718 });
2719 }
2720
2721 // Observe the main Elementor editor area
2722 var editorArea = document.querySelector('#elementor-editor-wrapper, .elementor-editor-wrapper');
2723 if (editorArea) {
2724 var editorObserver = createObserver();
2725 editorObserver.observe(editorArea, {
2726 childList: true,
2727 subtree: true
2728 });
2729 }
2730 }
2731
2732 /**
2733 * Initialize the translator
2734 */
2735 function initTranslator() {
2736 // Wait for Elementor to be fully loaded
2737 $(window).on('elementor:init', function() {
2738 // Add small delay to ensure Material UI is rendered
2739 setTimeout(onElementorInit, 500);
2740 });
2741
2742 // Fallback if elementor:init doesn't fire
2743 setTimeout(function() {
2744 onElementorInit();
2745 }, 3000);
2746
2747 // Additional fallback for when Material UI components are ready
2748 setTimeout(function() {
2749 if (!document.querySelector('.king-addons-ai-translator-btn')) {
2750 onElementorInit();
2751 }
2752 }, 5000);
2753 }
2754
2755 // Initialize when DOM is ready
2756 $(document).ready(function() {
2757 initTranslator();
2758
2759 // Global event handler for close buttons (backup protection)
2760 $(document).off('click.aiTranslatorGlobal').on('click.aiTranslatorGlobal', '.king-addons-translator-close-btn', function(e) {
2761 if (translationState.isTranslating) {
2762 stopTranslationProcess();
2763
2764 // Show cancellation notice
2765 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;">Translation cancelled</div>');
2766 $('body').append($notice);
2767 setTimeout(function() {
2768 $notice.fadeOut(300, function() {
2769 $notice.remove();
2770 });
2771 }, 2000);
2772 }
2773
2774 // Close popup/overlay
2775 var $popup = $(this).closest('.king-addons-translator-popup');
2776 if ($popup.hasClass('compact')) {
2777 $popup.remove();
2778 } else {
2779 $popup.closest('.king-addons-translator-overlay').remove();
2780 }
2781
2782 // Reset state and re-enable button
2783 translationState.isTranslating = false;
2784 translationState.isCancelled = false;
2785 translationState.currentRequests = [];
2786 toggleTranslatorButton(false);
2787
2788 e.preventDefault();
2789 e.stopPropagation();
2790 });
2791 });
2792
2793 })(jQuery, window.elementor);