PluginProbe
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI / 2.32.6
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI v2.32.6
2.34.5 2.34.4 2.34.3 2.34.2 2.34.1 2.34 2.33.10 2.33.9 2.33.8 2.33.7 2.33.6 2.33.5 2.33.2 2.32.10 2.33 2.33.1 2.32.6 2.32.7 2.32.8 trunk 2.10.3 2.10.4 2.10.5 2.10.6 2.11 All 57 releases
gptranslate / gptranslate.php

gptranslate.php in GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI 2.32.6, at gptranslate.php

4,227 lines 269.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: GPTranslate
4 Plugin URI: https://gptranslate.storejextensions.org/
5 Description: GPTranslate for Wordpress is the revolutionary multilanguage solution to automatically translate your Wordpress website thanks to the power of Artificial Intelligence like ChatGPT, Deepseek, Gemini, Claude, DeepL and more. ⚠️GPTranslate FREE Mode active
6 Author: JExtensions Store
7 Version: 2.32.6
8 Author URI: https://storejextensions.org
9 License: GPLv2 or later
10 License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 */
12
13 if (!defined('ABSPATH')) exit;
14
15 require_once plugin_dir_path(__FILE__) . 'includes/class-gptranslate-optimizer-compat.php';
16
17 class GPTranslate {
18 private static $instance = null;
19 private $table_name;
20 private $version;
21
22 private function isSelected($current, $value) {
23 return selected($current, $value, false);
24 }
25
26 public static $pluginVersion = '2.32.6';
27
28 /**
29 * Class constructor and settings inizializer with register_setting
30 *
31 * @access public
32 */
33 public function __construct() {
34 global $wpdb;
35 $this->table_name = $wpdb->prefix . 'gptranslate';
36
37 $this->version = '2.32.6';
38
39 $settings = get_option ( 'gptranslate_options', [ ] );
40
41 // Server-side plugin exclusion/inclusion check for frontend pages
42 if (! is_admin ()) {
43 $page_inclusions_raw = $settings ['page_inclusions'] ?? '';
44 $page_exclusions_raw = $settings ['page_exclusions'] ?? '';
45
46 // If page_inclusions is set, it takes priority over page_exclusions
47 if (! empty ( $page_inclusions_raw )) {
48
49 // Normalize input: newlines -> commas, collapse multiple commas
50 $inclusion_patterns = explode ( ',', trim ( preg_replace ( '/,+/', ',', str_ireplace ( [
51 "\r",
52 "\n"
53 ], ',', $page_inclusions_raw ) ), ',' ) );
54
55 // Normalize current request URI (PATH ONLY)
56 $request_uri = $_SERVER ['REQUEST_URI'] ?? '/';
57 $request_path = parse_url ( $request_uri, PHP_URL_PATH ) ?: '/';
58 $request_path = '/' . trim ( $request_path, '/' );
59
60 // Split path into segments
61 $path_segments = array_values ( array_filter ( explode ( '/', trim ( $request_path, '/' ) ) ) );
62
63 // Remove subfolder installation segment if configured
64 if (! empty ( $settings ['subfolder_installation'] ) && ! empty ( $path_segments )) {
65 array_shift ( $path_segments );
66 }
67
68 // Remove language slug if present (use configured languages)
69 $languages = (isset ( $settings ['languages'] ) && is_array ( $settings ['languages'] )) ? array_map ( 'strtolower', $settings ['languages'] ) : [ ];
70
71 if (! empty ( $path_segments ) && in_array ( strtolower ( $path_segments [0] ), $languages, true )) {
72 array_shift ( $path_segments );
73 }
74
75 // HOME detection:
76 // After removing subfolder + language, nothing left = HOME
77 $is_home_request = empty ( $path_segments );
78
79 // Check if current page is explicitly INCLUDED
80 $page_is_included = false;
81
82 // 1) Explicit HOME inclusion via "home" or "/"
83 if ($is_home_request) {
84 foreach ( $inclusion_patterns as $pattern ) {
85 $pattern = strtolower ( trim ( $pattern ) );
86 if ($pattern === 'home' || $pattern === '/') {
87 $page_is_included = true;
88 break;
89 }
90 }
91 }
92
93 // 2) Normal URL-based inclusion (full URL substring match)
94 if (! $page_is_included) {
95 $current_url = esc_url_raw ( ((! empty ( $_SERVER ['HTTPS'] ) && $_SERVER ['HTTPS'] !== 'off') ? 'https://' : 'http://') . ($_SERVER ['HTTP_HOST'] ?? '') . $request_uri );
96
97 foreach ( $inclusion_patterns as $pattern ) {
98 $pattern = trim ( $pattern );
99
100 // Skip empty and home patterns (already handled)
101 if ($pattern === '' || $pattern === '/' || strtolower ( $pattern ) === 'home') {
102 continue;
103 }
104
105 // Case-insensitive substring match
106 if (stripos ( $current_url, $pattern ) !== false) {
107 $page_is_included = true;
108 break;
109 }
110 }
111 }
112
113 // If page is not included, skip plugin execution
114 if (! $page_is_included) {
115 return;
116 }
117
118 } else if (! empty ( $page_exclusions_raw )) {
119 // If page_inclusions is empty, use page_exclusions logic
120
121 // Normalize input: newlines -> commas, collapse multiple commas
122 $patterns = explode ( ',', trim ( preg_replace ( '/,+/', ',', str_ireplace ( [
123 "\r",
124 "\n"
125 ], ',', $page_exclusions_raw ) ), ',' ) );
126
127 // Normalize current request URI (PATH ONLY)
128 $request_uri = $_SERVER ['REQUEST_URI'] ?? '/';
129 $request_path = parse_url ( $request_uri, PHP_URL_PATH ) ?: '/';
130 $request_path = '/' . trim ( $request_path, '/' );
131
132 // Split path into segments
133 $path_segments = array_values ( array_filter ( explode ( '/', trim ( $request_path, '/' ) ) ) );
134
135 // Remove subfolder installation segment if configured
136 if (! empty ( $settings ['subfolder_installation'] ) && ! empty ( $path_segments )) {
137 array_shift ( $path_segments );
138 }
139
140 // Remove language slug if present (use configured languages)
141 $languages = (isset ( $settings ['languages'] ) && is_array ( $settings ['languages'] )) ? array_map ( 'strtolower', $settings ['languages'] ) : [ ];
142
143 if (! empty ( $path_segments ) && in_array ( strtolower ( $path_segments [0] ), $languages, true )) {
144 array_shift ( $path_segments );
145 }
146
147 // HOME detection:
148 // After removing subfolder + language, nothing left = HOME
149 $is_home_request = empty ( $path_segments );
150
151 // 1) Explicit HOME exclusion via "home" or "/"
152 if ($is_home_request) {
153 foreach ( $patterns as $pattern ) {
154 $pattern = strtolower ( trim ( $pattern ) );
155 if ($pattern === 'home' || $pattern === '/') {
156 return; // Skip plugin execution
157 }
158 }
159 }
160
161 // 2) Normal URL-based exclusion (full URL substring match)
162 $current_url = esc_url_raw ( ((! empty ( $_SERVER ['HTTPS'] ) && $_SERVER ['HTTPS'] !== 'off') ? 'https://' : 'http://') . ($_SERVER ['HTTP_HOST'] ?? '') . $request_uri );
163
164 foreach ( $patterns as $pattern ) {
165 $pattern = trim ( $pattern );
166
167 // Skip empty and home patterns (already handled)
168 if ($pattern === '' || $pattern === '/' || strtolower ( $pattern ) === 'home') {
169 continue;
170 }
171
172 // Case-insensitive substring match
173 if (stripos ( $current_url, $pattern ) !== false) {
174 return; // Skip plugin execution
175 }
176 }
177 }
178 }
179
180 /**
181 * Global helper function to check if a link should be excluded from rewriting
182 */
183 if (!function_exists('gptranslate_should_exclude_link')) {
184 function gptranslate_should_exclude_link($href, $exclusions_raw) {
185 if (empty($exclusions_raw)) {
186 return false;
187 }
188
189 // Parse exclusion patterns: split by comma and newline, trim whitespace
190 $exclusion_patterns = explode(',', trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ',', $exclusions_raw)), ','));
191
192 // Decode href for comparison
193 $decoded_href = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
194
195 // Check each pattern using case-insensitive substring matching
196 foreach ($exclusion_patterns as $pattern) {
197 $pattern = trim($pattern);
198 if (empty($pattern)) {
199 continue;
200 }
201 // Case-insensitive substring match
202 if (stripos($decoded_href, $pattern) !== false) {
203 return true;
204 }
205 }
206
207 return false;
208 }
209 }
210
211 // Include various functions like multilanguage URLs, hreflang tag, HTML lang attribute rewriting
212 require_once plugin_dir_path(__FILE__) . 'multilang-routing.php';
213
214 if ( isset($settings ['serverside_translations']) && $settings ['serverside_translations'] == 1 ) {
215 require_once plugin_dir_path(__FILE__) . 'serverside-translations.php';
216 }
217
218 register_activation_hook ( __FILE__, [
219 $this,
220 'activate_plugin'
221 ] );
222
223 add_action ( 'admin_init', function () {
224 // Disable WordPress emoji script and styles
225 remove_action('wp_head', 'print_emoji_detection_script', 7);
226 remove_action('admin_print_scripts', 'print_emoji_detection_script');
227 remove_action('wp_print_styles', 'print_emoji_styles');
228 remove_action('admin_print_styles', 'print_emoji_styles');
229 remove_filter('the_content_feed', 'wp_staticize_emoji');
230 remove_filter('comment_text_rss', 'wp_staticize_emoji');
231 remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
232
233 function gptranslate_sanitize_options( $options ) {
234 $clean = [];
235 $clean = $options;
236
237 // Ensure original language is always included in enabled languages
238 if ( isset( $clean['language'] ) && ! empty( $clean['language'] ) ) {
239 $originalLang = strtolower( sanitize_text_field( $clean['language'] ) );
240 if ( ! isset( $clean['languages'] ) || ! is_array( $clean['languages'] ) ) {
241 $clean['languages'] = [];
242 }
243 $cleanedLangs = array_map( 'strtolower', $clean['languages'] );
244 if ( ! in_array( $originalLang, $cleanedLangs, true ) ) {
245 array_unshift( $clean['languages'], $originalLang );
246 }
247 }
248
249 return $clean;
250 }
251 register_setting ( 'gptranslate_settings', 'gptranslate_options', [
252 'sanitize_callback' => 'gptranslate_sanitize_options'
253 ]);
254
255 // Register record deletion
256 $page = sanitize_key($_GET['page'] ?? '');
257 $action = sanitize_key($_GET['action'] ?? '');
258 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
259 $translation_id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
260
261 if ($page === 'gptranslate' &&
262 $action === 'delete_translation' &&
263 $translation_id &&
264 wp_verify_nonce($nonce, 'gptranslate_delete_' . $translation_id)
265 ) {
266 $this->gptranslate_handle_deletion($translation_id);
267 exit;
268 }
269
270 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'toggle_published') {
271 // Toggle published state
272 $id = isset($_GET['translation_id']) ? (int) $_GET['translation_id'] : 0;
273
274 if (!$id || !isset($_GET['_gptranslate_nonce']) || !wp_verify_nonce(wp_unslash($_GET['_gptranslate_nonce']), 'gptranslate_toggle_' . $id)) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
275 wp_die('Invalid nonce.');
276 }
277
278 global $wpdb;
279 $table = $wpdb->prefix . 'gptranslate';
280
281 // Toggle published flag
282 $current = $wpdb->get_var($wpdb->prepare("SELECT published FROM $table WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
283 $new = ($current == 1) ? 0 : 1;
284
285 $wpdb->update($table, ['published' => $new], ['id' => $id]); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
286
287 wp_redirect(admin_url('admin.php?page=gptranslate&action=state_toggled'));
288 exit;
289 }
290
291 // Check for plugin update
292 function check_for_gptranslate_update($currentVersion) {
293 // Start the session if not already started
294 if (session_status() === PHP_SESSION_NONE) {
295 session_start();
296 }
297
298 // Reset session vars after update
299 if (isset($_SESSION['gptranslate_update_version']) && version_compare($currentVersion, $_SESSION['gptranslate_update_version'], '>=')) {
300 unset($_SESSION['gptranslate_update_version']);
301 unset($_SESSION['gptranslate_update_checked']);
302 }
303
304 // Check if the update check has been done in this session
305 if (!isset($_SESSION['gptranslate_update_checked']) || $_SESSION['gptranslate_update_checked'] !== true) {
306
307 // Perform the remote XML check
308 $remote_url = 'https://storejextensions.org/updates/gptranslatewp_updater.xml';
309 $response = wp_remote_get($remote_url);
310
311 if (!is_wp_error($response)) {
312 $body = wp_remote_retrieve_body($response);
313 if (!empty($body)) {
314 $xml = simplexml_load_string($body);
315 if ($xml && !empty($xml->update->version)) {
316 $updateversion = (string)$xml->update->version;
317 if (version_compare($updateversion, $currentVersion, '>')) {
318 // Store the update info in session (version, and flag that update is available)
319 $_SESSION['gptranslate_update_version'] = $updateversion;
320 }
321 $_SESSION['gptranslate_update_checked'] = true;
322 }
323 }
324 }
325 }
326
327 // If update is available, show the notice
328 $gpt_update_version = (isset($_SESSION['gptranslate_update_version']) && sanitize_text_field($_SESSION['gptranslate_update_version']))
329 ? sanitize_text_field($_SESSION['gptranslate_update_version'])
330 : '';
331 session_write_close();
332 if ($gpt_update_version) {
333 add_action('admin_notices', function () use ($gpt_update_version) {
334 echo '<div class="notice notice-warning is-dismissible">';
335 echo '<p>An update for <strong><a href="https://storejextensions.org/extensions/gptranslate.html" target="_blank">GPTranslate</a></strong> is available. The new version <strong>' . esc_html($gpt_update_version) . '</strong> can be downloaded from your reserved area if you have a valid subscription and license for the full version.</p>';
336 echo '</div>';
337 });
338 }
339 }
340 //check_for_gptranslate_update($this->version);
341 } );
342
343 // Post admin notices after actions
344 add_action( 'admin_notices', function() {
345 if ( isset( $_GET['page'], $_GET['deleted'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
346 if ( (int) $_GET['deleted'] === 1 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
347 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED')) . '</p></div>';
348 } elseif ( (int) $_GET['deleted'] === 0 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
349 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETED_ERROR')) . '</p></div>';
350 }
351 }
352
353 if ( isset( $_GET['page'], $_GET['action'] ) && sanitize_key($_GET['page']) === 'gptranslate' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
354 if ( $_GET['action'] === 'state_toggled' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
355 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_STATE_UPDATED_SUCCESSFULLY')) . '</p></div>';
356 }
357 }
358
359 if (isset($_GET['imported']) && $_GET['imported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
360 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_IMPORTED_SUCCESSFULLY')) . '</p></div>';
361 }
362
363 if (isset($_GET['settingsimported']) && $_GET['settingsimported'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
364 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_IMPORTED_SUCCESSFULLY')) . '</p></div>';
365 }
366 });
367
368 // Handle review notice dismiss/remind actions
369 add_action('admin_post_gptranslate_review_dismiss', function() {
370 check_admin_referer('gptranslate_review_dismiss');
371 $type = isset($_GET['type']) ? sanitize_key($_GET['type']) : 'forever';
372 if ($type === 'later') {
373 update_option('gptranslate_review_remind_later', time() + 14 * DAY_IN_SECONDS);
374 } else {
375 update_option('gptranslate_review_dismissed', true);
376 }
377 $redirect = wp_get_referer();
378 wp_safe_redirect($redirect ? $redirect : admin_url('admin.php?page=gptranslate-settings'));
379 exit;
380 });
381
382 // Add hook for admin menu links
383 add_action ( 'admin_menu', [
384 $this,
385 'admin_menu'
386 ] );
387
388 // Review button on all GPTranslate admin pages
389 add_action('admin_footer', function() {
390 $screen = get_current_screen();
391 if (!$screen || strpos($screen->id, 'gptranslate') === false) return;
392 $dismissed = get_option('gptranslate_review_dismissed');
393 $remind = get_option('gptranslate_review_remind_later');
394 if ($dismissed || ($remind && time() < (int)$remind)) return;
395 $url_review = 'https://wordpress.org/plugins/gptranslate/';
396 $url_later = esc_url(wp_nonce_url(admin_url('admin-post.php?action=gptranslate_review_dismiss&type=later'), 'gptranslate_review_dismiss'));
397 $url_forever = esc_url(wp_nonce_url(admin_url('admin-post.php?action=gptranslate_review_dismiss&type=forever'), 'gptranslate_review_dismiss'));
398 ?>
399 <button type="button" id="gpt-rate-btn" onclick="document.getElementById('gpt-review-overlay').classList.add('open')">
400 <span class="gpt-heart">&#10084;</span> Rate us <span class="gpt-stars">&#9733;&#9733;&#9733;&#9733;&#9733;</span>
401 </button>
402 <div id="gpt-review-overlay" onclick="if(event.target===this)this.classList.remove('open')">
403 <div id="gpt-review-box">
404 <button type="button" id="gpt-review-close" onclick="document.getElementById('gpt-review-overlay').classList.remove('open')" title="Close">&times;</button>
405 <div class="gpt-review-stars">&#9733;&#9733;&#9733;&#9733;&#9733;</div>
406 <h3 class="gpt-review-title">Enjoying GPTranslate?</h3>
407 <p class="gpt-review-desc">If GPTranslate has been helpful for your site, please take a moment to leave a 5-star review on WordPress.org your feedback really helps the plugin grow and improve!</p>
408 <a href="<?php echo esc_url($url_review); ?>" target="_blank" rel="noopener" class="gpt-review-cta">&#11088; Leave a 5-star review</a>
409 <div class="gpt-review-links">
410 <a href="<?php echo $url_later; ?>">&#128337; Remind me later</a>
411 <a href="<?php echo $url_forever; ?>">&#10003; Already did</a>
412 </div>
413 <div class="gpt-review-footer">Made with <span style="color:#e03e3e;">&#10084;</span> by <a href="https://storejextensions.org" target="_blank" rel="noopener">JExtensions Store</a></div>
414 </div>
415 </div>
416 <?php
417 });
418
419 // Add hook for record saving/deleting
420 add_action ( 'admin_post_save_gptranslate_record', [
421 $this,
422 'save_record'
423 ] );
424
425 add_action( 'admin_post_save_gptranslate_record_and_close', [
426 $this,
427 'save_record'
428 ]);
429
430 add_action('admin_post_cancel_gptranslate_record', [
431 $this,
432 'save_record'
433 ]);
434
435 // Add hook for adding main frontend app scripts
436 add_action ( 'wp_enqueue_scripts', [
437 $this,
438 'enqueue_frontend_scripts'
439 ] );
440 }
441
442 /**
443 * Singleton class instance
444 *
445 * @access public
446 */
447 public static function get_instance() {
448 if (null === self::$instance) {
449 self::$instance = new static();
450 }
451 return self::$instance;
452 }
453
454 /**
455 * Activation plugin hook with db table creation
456 *
457 * @access public
458 */
459 public function activate_plugin() {
460 global $wpdb;
461 $charset_collate = $wpdb->get_charset_collate();
462
463 $sql = "CREATE TABLE " . $this->table_name . " (
464 id int UNSIGNED NOT NULL AUTO_INCREMENT,
465 pagelink varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
466 translated_alias varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
467 translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
468 alt_translations mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
469 languageoriginal char(20) NOT NULL,
470 languagetranslated char(20) NOT NULL,
471 published tinyint NOT NULL DEFAULT '1',
472 translate_date datetime DEFAULT NULL,
473 translation_engine varchar(20) NOT NULL,
474 PRIMARY KEY (id),
475 INDEX idx_lookup (languageoriginal, languagetranslated, published, pagelink),
476 INDEX idx_alias_lookup (languageoriginal, languagetranslated, published, translated_alias)
477 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
478
479 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
480 dbDelta($sql);
481
482 // Valori di default
483 $default_options = [
484 'google_translate_engine' => '1',
485 'google_translate_method' => '0',
486 'chatgpt_apikey' => '',
487 'chatgpt_model' => 'gpt-3.5-turbo',
488 'chatgpt_request_message' => "Compile this JSON object key-value pairs adding the translation into '{{target}}' language to the empty value from the original '{{source}}' language of the key and return me only a parsable JSON object without any surrounding characters, preserve and return in the JSON object the key in the original '{{source}}' language within double quotes: '{{translations}}'. Pay attention to not skip any key and translate all keys. Return only a parsable JSON object with no surrounding text, explanations, or markdown formatting. Ensure the response is valid JSON and can be parsed directly.",
489 'chatgpt_request_conversation_mode' => 'user',
490 'language' => 'en',
491 'max_translations_per_request' => '100',
492 'max_characters_per_request' => '2048',
493 'detect_browser_language' => '0',
494 'autotranslate_detected_language' => '0',
495 'always_detect_autotranslated_language' => '0',
496 'auto_set_language_direction' => '0',
497 'serverside_translations' => '0',
498 'serverside_translations_method' => 'regex',
499 'serverside_translations_caseinsensitive' => '1',
500 'serverside_translations_matchquotes' => '1',
501 'serverside_translations_urldecode' => '1',
502 'serverside_translations_language_switching_mode' => 'url',
503 'serverside_translations_ignore_querystring' => '0',
504 'serverside_translations_strip_querystring_params' => '',
505 'serverside_translations_urlencode_space' => '0',
506 'css_selector_serverside_leafnodes_excluded' => '',
507 'detect_current_language' => '0',
508 'detect_default_language' => '0',
509 'rewrite_language_url' => '0',
510 'rewrite_language_alias' => '0',
511 'rewrite_language_alias_original_language' => '0',
512 'rewrite_page_links' => '0',
513 'rewrite_page_links_exclusions' => '',
514 'rewrite_form_actions' => '0',
515 'transliterate_urls' => '0',
516 'omit_prefix_original_language' => '0',
517 'excluded_alias_slugs' => '',
518 'rewrite_default_language_url' => '0',
519 'translate_metadata' => '0',
520 'set_html_lang' => '0',
521 'add_canonical' => '0',
522 'add_alternate' => '0',
523 'translate_placeholders' => '0',
524 'translate_altimages' => '0',
525 'css_selector_classes_translate_altimages_excluded' => '',
526 'translate_srcimages' => '0',
527 'translate_srciframes' => '0',
528 'translate_srcvideos' => '0',
529 'translate_iframe_locale' => '0',
530 'translate_titles' => '0',
531 'translate_values' => '0',
532 'metadata_chosen_engine' => '0',
533 'metadata_words_leafnodes_excluded' => '',
534 'default_language_first' => '0',
535 'css_selector_leafnodes_excluded' => 'a.nturl,.gt-lang-code',
536 'words_leafnodes_excluded' => '',
537 'words_leafnodes_excluded_bylanguage_repeatable' => '[]',
538 'words_min_length' => '',
539 'flatten_inner_formatting_tags' => '0',
540 'flatten_inner_formatting_tags_to_remove' => 'span,b,strong,i,em,u,font',
541 'wrap_excluded_words' => '0',
542 'apply_dictionary_to_aliases' => '0',
543 'crawler_timeout' => '30',
544 'crawler_exclusions' => '',
545 'crawler_skip_translated' => '0',
546 'page_exclusions' => '',
547 'page_inclusions' => '',
548 'chatgpt_gtranslate_request_delay' => '0',
549 'initial_translation_delay' => '0',
550 'realtime_translations' => '0',
551 'css_selector_realtime_translations_retrigger' => '',
552 'realtime_translations_retrigger_events' => ['click'],
553 'realtime_translations_retrigger_events_delay' => '200',
554 'realtime_translations_retrigger_force_google' => '0',
555 'realtime_translations_retrigger_do_not_revert' => '0',
556 'translations_export_format' => '.csv',
557 'ignore_querystring' => '0',
558 'enable_indexer' => '0',
559 'lightweight_ajax_endpoint' => '0',
560 'storage_type' => 'session',
561 'subfolder_installation' => '0',
562 'alt_flags' => [],
563 'languages' => ['en', 'es', 'de', 'it', 'fr'],
564 'excluded_languages' => [],
565 'enable_reader' => '0',
566 'responsivevoice_apikey' => 'PEVOFBma',
567 'responsivevoice_language_gender' => 'auto',
568 'responsivevoice_volume_tts' => '100',
569 'responsivevoice_voice_speed' => 'normal',
570 'mainpage_selector' => '*[name*=main], *[class*=main], *[id*=main], *[id*=container], *[class*=container]',
571 'elements_toexclude_custom' => '',
572 'proxy_responsive_loading_script' => '1',
573 'proxy_responsive_reading_mode' => 'native',
574 'chunksize' => '200',
575 'widget_text_color' => '#000000',
576 'widget_background_color' => '#FFFFFF',
577 'popup_border_radius' => '0',
578 'popup_fontsize' => '20',
579 'popup_iconsize' => '32',
580 'popup_shadow' => '1',
581 'disable_toast_popups' => '0',
582 'widget_opacity' => '1.0',
583 'float_position' => 'bottom-left',
584 'float_switcher_open_direction' => 'top',
585 'flag_style' => '2d',
586 'flag_loading' => 'local',
587 'show_language_titles' => '1',
588 'enable_dropdown' => '1',
589 'enable_modal' => '0',
590 'equal_widths' => '0',
591 'reader_button_position' => 'top',
592 'widget_max_height' => '260',
593 'wrapper_selector' => '.gptranslate_wrapper',
594 'draggable_widget' => '0',
595 'disable_control' => '0',
596 'custom_css' => '',
597 'disable_bootstrap_css' => '0',
598 'main_script_integrity_check' => '1',
599 'lock_translations' => '1',
600 'incremental_enabled' => '0',
601 'incremental_show_progress_popup' => '0',
602 'incremental_excluded_css_selectors' => '#wpadminbar,.post-views-count,.post-meta-views,.entry-views,.views-count,.comment-count,.comments-link,.entry-date,.post-date,.publish-date,.updated,.modified,.wp-calendar,.calendar,.widget_recent_posts,.widget_recent_comments,.elementor-counter,.elementor-number-counter,.jet-counter,.counter-box,.rank-math-breadcrumb,.breadcrumb-trail,.wp-pagenumbers,.pagination,.rss-date,.post-meta-date,.social-count,.share-count,.read-more,.more-link,.author-name',
603 'incremental_excluded_text_patterns' => '',
604 'incremental_preserve_new_texts' => '1',
605 'incremental_caseinsensitive' => '0',
606 'incremental_filter_excluded_words' => '0',
607 'incremental_filter_dictionary_keys' => '0',
608 ];
609
610 // Se l'opzione non è ancora presente, la crea
611 if (get_option('gptranslate_options') === false) {
612 add_option('gptranslate_options', $default_options);
613 // New clean install: show guided wizard once (no forced redirect).
614 add_option('gptranslate_wizard_completed', 0, '', false);
615 add_option('gptranslate_wizard_first_redirect_done', 0, '', false);
616 }
617
618 }
619
620 /**
621 * Function to add admin menu for both settings and translations management
622 *
623 * @access public
624 */
625 public function admin_menu() {
626 add_menu_page('GPTranslate', 'GPTranslate', 'manage_options', 'gptranslate', [$this, 'records_page'], 'dashicons-translation');
627
628 // Add a submenu that matches the main menu to prevent duplication and allow renaming
629 add_submenu_page('gptranslate', esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')), 'manage_options', 'gptranslate', [$this, 'records_page']);
630
631 // Now you can safely add a differently named submenu
632 add_submenu_page('gptranslate', esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')), 'manage_options', 'gptranslate-settings', [$this, 'settings_page']);
633 add_submenu_page('gptranslate', 'Wizard', 'Wizard', 'manage_options', 'gptranslate-wizard', [$this, 'wizard_page']);
634 }
635
636 /**
637 * Load the configuration settings page held in the settings.php file
638 *
639 * @access public
640 */
641 public function settings_page() {
642 require_once 'settings.php';
643
644 echo '<script>
645 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
646 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
647 </script>';
648 }
649
650 public function wizard_page() {
651 $options = get_option('gptranslate_options', []);
652 $currentLanguage = isset($options['language']) ? (string)$options['language'] : 'en';
653 $currentEngine = isset($options['google_translate_engine']) ? (string)$options['google_translate_engine'] : '1';
654 $currentModel = isset($options['chatgpt_model']) ? (string)$options['chatgpt_model'] : 'gpt-5.5';
655 $currentApiKey = isset($options['chatgpt_apikey']) ? (string)$options['chatgpt_apikey'] : '';
656 $currentServerside = isset($options['serverside_translations']) ? (string)$options['serverside_translations'] : '0';
657 $currentServersideMethod = isset($options['serverside_translations_method']) ? (string)$options['serverside_translations_method'] : 'regex';
658 $currentMetadata = isset($options['translate_metadata']) ? (string)$options['translate_metadata'] : '0';
659 $currentSetHtmlLang = isset($options['set_html_lang']) ? (string)$options['set_html_lang'] : '0';
660 $currentAddCanonical = isset($options['add_canonical']) ? (string)$options['add_canonical'] : '0';
661 $currentAddAlternate = isset($options['add_alternate']) ? (string)$options['add_alternate'] : '0';
662 $currentRewriteLanguageUrl = isset($options['rewrite_language_url']) ? (string)$options['rewrite_language_url'] : '0';
663 $currentRewriteLanguageAlias = isset($options['rewrite_language_alias']) ? (string)$options['rewrite_language_alias'] : '0';
664 $currentRewritePageLinks = isset($options['rewrite_page_links']) ? (string)$options['rewrite_page_links'] : '0';
665 $currentPosition = isset($options['float_position']) ? (string)$options['float_position'] : 'bottom-left';
666 $currentEnabledLanguages = isset($options['languages']) && is_array($options['languages']) ? array_map('strtolower', $options['languages']) : ['en', 'it', 'de', 'fr', 'es', 'pt'];
667 $wizardLanguages = [];
668 $baseLanguageFile = plugin_dir_path(__FILE__) . 'language/en-GB/gptranslate.ini';
669 $baseLanguageStrings = file_exists($baseLanguageFile) ? parse_ini_file($baseLanguageFile, false, INI_SCANNER_NORMAL) : [];
670 if (is_array($baseLanguageStrings)) {
671 foreach ($baseLanguageStrings as $k => $v) {
672 if (strpos($k, 'PLG_GPTRANSLATE_LANGUAGE_NAME_') === 0) {
673 $suffix = substr($k, strlen('PLG_GPTRANSLATE_LANGUAGE_NAME_'));
674 $langCode = strtolower(str_replace('_', '-', $suffix));
675 if ($langCode !== '') {
676 $wizardLanguages[$langCode] = $this->loadTranslations($k);
677 }
678 }
679 }
680 }
681 if (empty($wizardLanguages)) {
682 $wizardLanguages = ['en' => 'English', 'it' => 'Italian', 'de' => 'German', 'fr' => 'French', 'es' => 'Spanish'];
683 }
684 asort($wizardLanguages, SORT_NATURAL | SORT_FLAG_CASE);
685 ?>
686 <div class="gpw-wrap">
687 <div class="gpw-shell">
688 <div class="gpw-breadcrumb">WordPress Admin &rsaquo; GPTranslate &rsaquo; Wizard</div>
689 <div class="gpw-card">
690 <div class="gpw-fun-layer" aria-hidden="true">
691 <div class="gpw-orb gpw-orb-a"></div>
692 <div class="gpw-orb gpw-orb-b"></div>
693 <div class="gpw-orb gpw-orb-c"></div>
694 <div class="gpw-filament gpw-filament-a"></div>
695 <div class="gpw-filament gpw-filament-b"></div>
696 </div>
697 <div class="gpw-burst-layer" id="gpw-burst-layer" aria-hidden="true"></div>
698 <canvas id="gpw-particles" class="gpw-particles" aria-hidden="true"></canvas>
699 <div class="gpw-progress">
700 <div class="gpw-step-slot"><div class="gpw-step is-active" data-stepdot="1">1</div><span class="gpw-step-label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_STEP_WELCOME')); ?></span></div><div class="gpw-line"></div>
701 <div class="gpw-step-slot"><div class="gpw-step" data-stepdot="2">2</div><span class="gpw-step-label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_STEP_LANGUAGE')); ?></span></div><div class="gpw-line"></div>
702 <div class="gpw-step-slot"><div class="gpw-step" data-stepdot="3">3</div><span class="gpw-step-label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_STEP_ENGINE')); ?></span></div><div class="gpw-line"></div>
703 <div class="gpw-step-slot"><div class="gpw-step" data-stepdot="4">4</div><span class="gpw-step-label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_STEP_SERVERSIDE')); ?></span></div><div class="gpw-line"></div>
704 <div class="gpw-step-slot"><div class="gpw-step" data-stepdot="5">5</div><span class="gpw-step-label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_STEP_METADATA')); ?></span></div><div class="gpw-line"></div>
705 <div class="gpw-step-slot"><div class="gpw-step" data-stepdot="6">6</div><span class="gpw-step-label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_STEP_REWRITE')); ?></span></div><div class="gpw-line"></div>
706 <div class="gpw-step-slot"><div class="gpw-step" data-stepdot="7">7</div><span class="gpw-step-label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_STEP_DONE')); ?></span></div>
707 </div>
708 <div class="gpw-content">
709 <section class="gpw-panel" data-step="1">
710 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_TITLE')); ?></h2>
711 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_DESC')); ?></p>
712 <div class="gpw-switchbox gpw-switchbox-row">
713 <div class="gpw-choice-card gpw-choice-card-single gpw-info-card">
714 <span class="gpw-info-icon dashicons dashicons-superhero-alt" aria-hidden="true"></span>
715 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_CARD1_TITLE')); ?></span>
716 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_CARD1_DESC')); ?></span>
717 </div>
718 <div class="gpw-choice-card gpw-choice-card-single gpw-info-card">
719 <span class="gpw-info-icon dashicons dashicons-chart-line" aria-hidden="true"></span>
720 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_CARD2_TITLE')); ?></span>
721 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_CARD2_DESC')); ?></span>
722 </div>
723 <div class="gpw-choice-card gpw-choice-card-single gpw-info-card">
724 <span class="gpw-info-icon dashicons dashicons-controls-repeat" aria-hidden="true"></span>
725 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_CARD3_TITLE')); ?></span>
726 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_WELCOME_CARD3_DESC')); ?></span>
727 </div>
728 </div>
729 </section>
730 <section class="gpw-panel" data-step="2" style="display:none;">
731 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_LANGUAGES_TITLE')); ?></h2>
732 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_LANGUAGES_DESC')); ?></p>
733 <div class="gpw-lang-grid">
734 <div class="gpw-lang-col">
735 <label for="gpw_language" class="gpw-field-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_ORIGINAL_LANGUAGE')); ?></label>
736 <select id="gpw_language">
737 <?php foreach ($wizardLanguages as $lCode => $lLabel) : ?>
738 <option value="<?php echo esc_attr($lCode); ?>" <?php selected($currentLanguage, $lCode); ?>><?php echo esc_html($lLabel); ?></option>
739 <?php endforeach; ?>
740 </select>
741 <p class="gpw-field-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_ORIGINAL_LANGUAGE_DESC')); ?></p>
742 </div>
743 <div class="gpw-lang-col">
744 <label class="gpw-field-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ENABLED_LANGUAGES')); ?></label>
745 <select id="gpw_languages_enabled" class="select2-dropdown" multiple>
746 <?php foreach ($wizardLanguages as $lCode => $lLabel) : ?>
747 <option value="<?php echo esc_attr($lCode); ?>" <?php selected(in_array($lCode, $currentEnabledLanguages, true)); ?>><?php echo esc_html($lLabel); ?></option>
748 <?php endforeach; ?>
749 </select>
750 <p class="gpw-field-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ENABLED_LANGUAGES_DESC')); ?></p>
751 </div>
752 </div>
753 </section>
754 <section class="gpw-panel" data-step="3" style="display:none;">
755 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_AI_PROVIDER_TITLE')); ?></h2>
756 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_AI_PROVIDER_DESC')); ?></p>
757 <select id="gpw_engine" style="display:none;">
758 <option value="1" <?php selected($currentEngine, '1'); ?>><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_GTRANSLATE')); ?></option>
759 <option value="0" <?php selected($currentEngine, '0'); ?>><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_CHATGPT')); ?></option>
760 </select>
761 <div class="gpw-provider-grid" id="gpw_provider_grid">
762 <button type="button" class="gpw-provider-card" data-provider="googleai">
763 <span class="gpw-provider-dot"></span>
764 <strong>Google AI</strong>
765 <small><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_GTRANSLATE')); ?></small>
766 </button>
767 <button type="button" class="gpw-provider-card" data-provider="openai">
768 <span class="gpw-provider-dot"></span>
769 <strong>OpenAI</strong>
770 <small>ChatGPT models</small>
771 </button>
772 <button type="button" class="gpw-provider-card" data-provider="claude">
773 <span class="gpw-provider-dot"></span>
774 <strong>Claude</strong>
775 <small>Anthropic models</small>
776 </button>
777 <button type="button" class="gpw-provider-card" data-provider="gemini">
778 <span class="gpw-provider-dot"></span>
779 <strong>Gemini</strong>
780 <small>Google Gemini models</small>
781 </button>
782 <button type="button" class="gpw-provider-card" data-provider="deepseek">
783 <span class="gpw-provider-dot"></span>
784 <strong>DeepSeek</strong>
785 <small>DeepSeek models</small>
786 </button>
787 <button type="button" class="gpw-provider-card" data-provider="grok">
788 <span class="gpw-provider-dot"></span>
789 <strong>Grok</strong>
790 <small>xAI models</small>
791 </button>
792 <button type="button" class="gpw-provider-card" data-provider="deepl">
793 <span class="gpw-provider-dot"></span>
794 <strong>DeepL</strong>
795 <small>DeepL API</small>
796 </button>
797 <button type="button" class="gpw-provider-card" data-provider="googlecloud">
798 <span class="gpw-provider-dot"></span>
799 <strong>Google Cloud</strong>
800 <small>Cloud Translation API</small>
801 </button>
802 </div>
803 <div id="gpw_model_wrap">
804 <label for="gpw_model" id="gpw_model_label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_CHOOSE_MODEL')); ?></label>
805 <select id="gpw_model">
806 <option value="gpt-3.5-turbo" <?php selected($currentModel, 'gpt-3.5-turbo'); ?>>GPT-3.5-Turbo</option>
807 <option value="gpt-4" <?php selected($currentModel, 'gpt-4'); ?>>GPT-4</option>
808 <option value="gpt-4o" <?php selected($currentModel, 'gpt-4o'); ?>>GPT-4o</option>
809 <option value="gpt-4o-mini" <?php selected($currentModel, 'gpt-4o-mini'); ?>>GPT-4o Mini</option>
810 <option value="gpt-4.1" <?php selected($currentModel, 'gpt-4.1'); ?>>GPT-4.1</option>
811 <option value="gpt-4.1-mini" <?php selected($currentModel, 'gpt-4.1-mini'); ?>>GPT-4.1 Mini</option>
812 <option value="gpt-4.1-nano" <?php selected($currentModel, 'gpt-4.1-nano'); ?>>GPT-4.1 Nano</option>
813 <option value="gpt-5" <?php selected($currentModel, 'gpt-5'); ?>>GPT-5</option>
814 <option value="gpt-5-mini" <?php selected($currentModel, 'gpt-5-mini'); ?>>GPT-5 Mini</option>
815 <option value="gpt-5-nano" <?php selected($currentModel, 'gpt-5-nano'); ?>>GPT-5 Nano</option>
816 <option value="gpt-5.1" <?php selected($currentModel, 'gpt-5.1'); ?>>GPT-5.1</option>
817 <option value="gpt-5.2" <?php selected($currentModel, 'gpt-5.2'); ?>>GPT-5.2</option>
818 <option value="gpt-5.4-mini" <?php selected($currentModel, 'gpt-5.4-mini'); ?>>GPT-5.4 Mini</option>
819 <option value="gpt-5.4-nano" <?php selected($currentModel, 'gpt-5.4-nano'); ?>>GPT-5.4 Nano</option>
820 <option value="gpt-5.4" <?php selected($currentModel, 'gpt-5.4'); ?>>GPT-5.4</option>
821 <option value="gpt-5.5" <?php selected($currentModel, 'gpt-5.5'); ?>>GPT-5.5</option>
822 <option value="deepseek-chat" <?php selected($currentModel, 'deepseek-chat'); ?>>DeepSeek Chat</option>
823 <option value="gemini-2.5-flash" <?php selected($currentModel, 'gemini-2.5-flash'); ?>>Gemini 2.5 Flash</option>
824 <option value="gemini-2.5-pro" <?php selected($currentModel, 'gemini-2.5-pro'); ?>>Gemini 2.5 Pro</option>
825 <option value="gemini-3-flash-preview" <?php selected($currentModel, 'gemini-3-flash-preview'); ?>>Gemini 3.1 Flash Preview</option>
826 <option value="gemini-3.1-flash-lite" <?php selected($currentModel, 'gemini-3.1-flash-lite'); ?>>Gemini 3.1 Flash Lite</option>
827 <option value="gemini-3.1-pro-preview" <?php selected($currentModel, 'gemini-3-pro-preview'); ?>>Gemini 3.1 Pro Preview</option>
828 <option value="claude-sonnet-4-5" <?php selected($currentModel, 'claude-sonnet-4-5'); ?>>Claude Sonnet 4.5</option>
829 <option value="claude-sonnet-4-6" <?php selected($currentModel, 'claude-sonnet-4-6'); ?>>Claude Sonnet 4.6</option>
830 <option value="claude-haiku-4-5" <?php selected($currentModel, 'claude-haiku-4-5'); ?>>Claude Haiku 4.5</option>
831 <option value="claude-opus-4-6" <?php selected($currentModel, 'claude-opus-4-6'); ?>>Claude Opus 4.6</option>
832 <option value="claude-opus-4-7" <?php selected($currentModel, 'claude-opus-4-7'); ?>>Claude Opus 4.7</option>
833 <option value="grok-3" <?php selected($currentModel, 'grok-3'); ?>>Grok 3</option>
834 <option value="grok-3-mini" <?php selected($currentModel, 'grok-3-mini'); ?>>Grok 3 Mini</option>
835 <option value="google-cloud-translation-api" <?php selected($currentModel, 'google-cloud-translation-api'); ?>>Google Cloud Translation API</option>
836 <option value="deepl-api" <?php selected($currentModel, 'deepl-api'); ?>>DeepL API</option>
837 </select>
838 <div class="gpw-apikey-wrap">
839 <label for="gpw_apikey" id="gpw_apikey_label" data-template="<?php echo esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_APIKEY')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_APIKEY')); ?></label>
840 <div class="gpw-apikey-inputrow">
841 <input type="password" id="gpw_apikey" value="<?php echo esc_attr($currentApiKey); ?>" autocomplete="off">
842 <button type="button" class="button gpw-apikey-toggle" id="gpw_apikey_toggle" title="<?php echo esc_attr($this->loadTranslations('PLG_GPTRANSLATE_APIKEY_SHOW')); ?>">
843 <span class="dashicons dashicons-visibility"></span>
844 </button>
845 <button type="button" class="button" id="gpw_apikey_test"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_TEST_APIKEY')); ?></button>
846 </div>
847 <div id="gpw_apikey_test_result" class="gpw-apikey-test-result" style="display:none;"></div>
848 </div>
849 </div>
850 <div id="gpw_google_method_wrap" class="gpw-method-card" style="display:none;">
851 <label for="gpw_google_method" class="gpw-method-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_GOOGLE_TRANSLATE_METHOD')); ?></label>
852 <select id="gpw_google_method">
853 <option value="0" <?php selected((string)($options['google_translate_method'] ?? '0'), '0'); ?>><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_GOOGLE_TRANSLATE_METHOD_STANDARD')); ?></option>
854 <option value="1" <?php selected((string)($options['google_translate_method'] ?? '0'), '1'); ?>><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_GOOGLE_TRANSLATE_METHOD_TRANSLATEHTML')); ?></option>
855 </select>
856 <p class="gpw-method-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_GOOGLE_TRANSLATE_METHOD_DESC')); ?></p>
857 </div>
858 </section>
859 <section class="gpw-panel" data-step="4" style="display:none;">
860 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS')); ?></h2>
861 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS_DESC')); ?></p>
862 <div class="gpw-switchbox">
863 <label class="gpw-choice-card gpw-choice-card-single<?php echo ($currentServerside === '1') ? ' is-selected' : ''; ?>">
864 <input type="checkbox" id="gpw_serverside_toggle" <?php checked($currentServerside, '1'); ?>>
865 <span class="gpw-choice-toggle"></span>
866 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS')); ?></span>
867 <span class="gpw-choice-desc">Enable server-side HTML output translation.</span>
868 </label>
869 </div>
870 <div id="gpw_serverside_method_wrap" class="gpw-method-card">
871 <label for="gpw_serverside_method" class="gpw-method-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS_METHOD')); ?></label>
872 <select id="gpw_serverside_method">
873 <option value="regex" <?php selected($currentServersideMethod, 'regex'); ?>><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS_METHOD_REGEX')); ?></option>
874 <option value="domdocument" <?php selected($currentServersideMethod, 'domdocument'); ?>><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS_METHOD_DOMDOCUMENT')); ?></option>
875 <option value="simplehtmldom" <?php selected($currentServersideMethod, 'simplehtmldom'); ?>><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS_METHOD_SIMPLEHTMLDOM')); ?></option>
876 </select>
877 <p class="gpw-method-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS_METHOD_DESC_WIZARD')); ?></p>
878 </div>
879 </section>
880 <section class="gpw-panel" data-step="5" style="display:none;">
881 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATE_METADATA')); ?></h2>
882 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATE_METADATA_DESC')); ?></p>
883 <div class="gpw-switchbox">
884 <label class="gpw-choice-card gpw-choice-card-single<?php echo ($currentMetadata === '1') ? ' is-selected' : ''; ?>">
885 <input type="checkbox" id="gpw_metadata_toggle" <?php checked($currentMetadata, '1'); ?>>
886 <span class="gpw-choice-toggle"></span>
887 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATE_METADATA')); ?></span>
888 <span class="gpw-choice-desc">Translate title, description and metadata.</span>
889 </label>
890 </div>
891 <div id="gpw_metadata_extras_wrap" class="gpw-switchbox gpw-switchbox-row" style="display:none;">
892 <label class="gpw-choice-card gpw-choice-card-single<?php echo ($currentAddCanonical === '1') ? ' is-selected' : ''; ?>">
893 <input type="checkbox" id="gpw_add_canonical" <?php checked($currentAddCanonical, '1'); ?>>
894 <span class="gpw-choice-toggle"></span>
895 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_CANONICAL')); ?></span>
896 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_CANONICAL_DESC_WIZARD')); ?></span>
897 </label>
898 <label class="gpw-choice-card gpw-choice-card-single<?php echo ($currentAddAlternate === '1') ? ' is-selected' : ''; ?>">
899 <input type="checkbox" id="gpw_add_alternate" <?php checked($currentAddAlternate, '1'); ?>>
900 <span class="gpw-choice-toggle"></span>
901 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_ALTERNATE')); ?></span>
902 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_ALTERNATE_DESC_WIZARD')); ?></span>
903 </label>
904 <label class="gpw-choice-card gpw-choice-card-single<?php echo ($currentSetHtmlLang === '1') ? ' is-selected' : ''; ?>">
905 <input type="checkbox" id="gpw_set_html_lang" <?php checked($currentSetHtmlLang, '1'); ?>>
906 <span class="gpw-choice-toggle"></span>
907 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SET_HTML_LANG')); ?></span>
908 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SET_HTML_LANG_DESC_WIZARD')); ?></span>
909 </label>
910 </div>
911 </section>
912 <section class="gpw-panel" data-step="6" style="display:none;">
913 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_URL_REWRITING_TITLE')); ?></h2>
914 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_URL_REWRITING_DESC')); ?></p>
915 <div class="gpw-switchbox gpw-switchbox-row gpw-rewrite-primary-row">
916 <label class="gpw-choice-card gpw-choice-card-single<?php echo ($currentRewriteLanguageUrl === '1') ? ' is-selected' : ''; ?>">
917 <input type="checkbox" id="gpw_rewrite_language_url" <?php checked($currentRewriteLanguageUrl, '1'); ?>>
918 <span class="gpw-choice-toggle"></span>
919 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_REWRITE_LANGUAGE_URL')); ?></span>
920 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_REWRITE_LANGUAGE_URL_DESC_WIZARD')); ?></span>
921 </label>
922 </div>
923 <div id="gpw_rewrite_children_wrap" class="gpw-switchbox gpw-switchbox-row gpw-rewrite-children-row">
924 <label id="gpw_rewrite_language_alias_wrap" class="gpw-choice-card gpw-choice-card-single<?php echo ($currentRewriteLanguageAlias === '1') ? ' is-selected' : ''; ?>">
925 <input type="checkbox" id="gpw_rewrite_language_alias" <?php checked($currentRewriteLanguageAlias, '1'); ?>>
926 <span class="gpw-choice-toggle"></span>
927 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_REWRITE_LANGUAGE_ALIAS')); ?></span>
928 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_REWRITE_LANGUAGE_ALIAS_DESC_WIZARD')); ?></span>
929 </label>
930 <label id="gpw_rewrite_page_links_wrap" class="gpw-choice-card gpw-choice-card-single<?php echo ($currentRewritePageLinks === '1') ? ' is-selected' : ''; ?>">
931 <input type="checkbox" id="gpw_rewrite_page_links" <?php checked($currentRewritePageLinks, '1'); ?>>
932 <span class="gpw-choice-toggle"></span>
933 <span class="gpw-choice-title"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_REWRITE_PAGE_LINKS')); ?></span>
934 <span class="gpw-choice-desc"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_REWRITE_PAGE_LINKS_DESC_WIZARD')); ?></span>
935 </label>
936 </div>
937 </section>
938 <section class="gpw-panel" data-step="7" style="display:none;">
939 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_POSITION')); ?></h2>
940 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_POSITION_DESC')); ?></p>
941 <select id="gpw_position">
942 <option value="bottom-left" <?php selected($currentPosition, 'bottom-left'); ?>>Bottom left</option>
943 <option value="bottom-right" <?php selected($currentPosition, 'bottom-right'); ?>>Bottom right</option>
944 <option value="top-left" <?php selected($currentPosition, 'top-left'); ?>>Top left</option>
945 <option value="top-right" <?php selected($currentPosition, 'top-right'); ?>>Top right</option>
946 <option value="inline" <?php selected($currentPosition, 'inline'); ?>>Inline</option>
947 </select>
948 <p id="gpw-inline-hint" class="gpw-inline-hint" style="display:none;">Use shortcode <code>[gptranslate]</code> when position is inline.</p>
949 <div id="gpw_completion_panel" class="gpw-completion" style="display:none;">
950 <h2><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_TITLE')); ?></h2>
951 <p><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_DESC')); ?></p>
952 <div class="gpw-completion-table">
953 <div class="gpw-completion-row"><strong><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_PROVIDER')); ?></strong><span id="gpw_done_provider"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
954 <div class="gpw-completion-row"><strong id="gpw_done_model_label"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_MODEL')); ?></strong><span id="gpw_done_model"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
955 <div class="gpw-completion-row"><strong><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_ORIGINAL_LANGUAGE')); ?></strong><span id="gpw_done_original_language"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
956 <div class="gpw-completion-row"><strong><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ENABLED_LANGUAGES')); ?></strong><span id="gpw_done_enabled_languages"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
957 <div class="gpw-completion-row"><strong><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SERVERSIDE_TRANSLATIONS')); ?></strong><span id="gpw_done_serverside"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
958 <div class="gpw-completion-row"><strong><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_URL_REWRITING_TITLE')); ?></strong><span id="gpw_done_rewrite"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
959 <div class="gpw-completion-row"><strong><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_METADATA')); ?></strong><span id="gpw_done_metadata"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
960 <div class="gpw-completion-row"><strong><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_POSITION')); ?></strong><span id="gpw_done_position"></span><a href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT')); ?></a></div>
961 </div>
962 <div class="gpw-completion-actions">
963 <a class="button button-primary" href="<?php echo esc_url(admin_url('admin.php?page=gptranslate')); ?>"><span class="dashicons dashicons-translation gpw-start-icon"></span><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_START_TRANSLATING')); ?></a>
964 <a class="button" href="<?php echo esc_url(admin_url('admin.php?page=gptranslate-settings')); ?>"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_DONE_EDIT_SETTINGS')); ?></a>
965 </div>
966 </div>
967 </section>
968 </div>
969 <div class="gpw-actions">
970 <button type="button" class="button" id="gpw_prev" disabled><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_PREVIOUS')); ?></button>
971 <button type="button" class="button button-primary" id="gpw_next"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_NEXT')); ?></button>
972 <button type="button" class="button button-primary" id="gpw_save" style="display:none;"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_SAVECONFIG')); ?></button>
973 </div>
974 </div>
975 <div class="gpw-footer"><?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_WIZARD_FOOTER')); ?></div>
976 </div>
977 </div>
978 <div class="gpw-hoppy-assistant" id="gpw_hoppy_assistant" aria-live="polite">
979 <button type="button" class="gpw-hoppy-close" id="gpw_hoppy_close" aria-label="Close">×</button>
980 <div class="gpw-hoppy-bubble" id="gpw_hoppy_bubble"></div>
981 <div class="gpw-hoppy-avatar"><img
982 id="gpw_hoppy_avatar_img"
983 src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-wave.svg', __FILE__)); ?>"
984 data-step1-src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-wave.svg', __FILE__)); ?>"
985 data-step2-src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-focus.svg', __FILE__)); ?>"
986 data-step3-src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-think.svg', __FILE__)); ?>"
987 data-step4-src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-alert.svg', __FILE__)); ?>"
988 data-step5-src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-eager.svg', __FILE__)); ?>"
989 data-step6-src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-focus.svg', __FILE__)); ?>"
990 data-step7-src="<?php echo esc_url(plugins_url('assets/images/wizard-hoppy-cheer.svg', __FILE__)); ?>"
991 alt="Hoppy"></div>
992 </div>
993 <?php
994 }
995
996 /**
997 * Translation records pages, list and edit
998 *
999 * @access public
1000 */
1001 public function records_page() {
1002 global $wpdb;
1003
1004 // Edit record
1005 if (isset($_GET['action']) && sanitize_key($_GET['action']) == 'edit') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1006 $id = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
1007 $nonce = isset($_GET['_gptranslate_nonce']) ? wp_unslash($_GET['_gptranslate_nonce']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1008 $opts = get_option( 'gptranslate_options', [] );
1009
1010 if ( ! wp_verify_nonce( $nonce, 'gptranslate_edit_' . $id ) ) {
1011 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
1012 }
1013
1014 $record = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1015
1016 $translationsArray = json_decode($record->translations, true) ?? [];
1017 $altTranslationsArray = json_decode($record->alt_translations, true) ?? [];
1018
1019 uksort($translationsArray, function($a, $b) {
1020 return strlen($b) - strlen($a);
1021 });
1022 uksort($altTranslationsArray, function($a, $b) {
1023 return strlen($b) - strlen($a);
1024 });
1025
1026 // Path relativo o assoluto all'immagine della bandiera
1027 $flagUrlOriginal = plugins_url('flags/svg/' . esc_attr($record->languageoriginal) . '.svg', __FILE__);
1028 $flagUrlTranslated = plugins_url('flags/svg/' . esc_attr($record->languagetranslated) . '.svg', __FILE__);
1029
1030 // Alternative flags check for edit view
1031 $altFlagsOpts = isset($opts['alt_flags']) && is_array($opts['alt_flags']) ? $opts['alt_flags'] : [];
1032 $altFlagMap = [
1033 'en' => ['usa' => 'en-us', 'canada' => 'en-ca', 'ireland' => 'en-ie'],
1034 'pt' => ['brazil' => 'pt-br'],
1035 'es' => ['mexico' => 'es-mx', 'argentina' => 'es-ar', 'colombia' => 'es-co'],
1036 'fr' => ['quebec' => 'fr-qc'],
1037 'zh' => ['taiwan' => 'zh-TW'],
1038 'zt' => ['hongkong' => 'zh-HK'],
1039 'de' => ['austria' => 'de-at'],
1040 ];
1041 foreach ($altFlagMap as $langCode => $variants) {
1042 foreach ($variants as $country => $flagFile) {
1043 if (in_array($country, $altFlagsOpts)) {
1044 if ($record->languageoriginal === $langCode) {
1045 $flagUrlOriginal = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1046 }
1047 if ($record->languagetranslated === $langCode) {
1048 $flagUrlTranslated = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1049 }
1050 break;
1051 }
1052 }
1053 }
1054
1055 $pubIcon = $record->published ? '<img src="' . plugins_url('assets/images/published.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . '">' // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1056 : '<img src="' . plugins_url('assets/images/unpublished.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . '">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1057 $rewriteAliasRow = '';
1058 if ($opts ['rewrite_language_alias'] == 1) {
1059 $rewriteAliasRow = '<tr>
1060 <th scope="row"><label for="translated_alias" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</label></th>
1061 <td><input type="text" id="translated_alias" name="translated_alias" value="' . esc_attr($record->translated_alias) . '" class="regular-text code"></td>
1062 </tr>';
1063 }
1064
1065 echo '<h1><img class="gptranslate-plugin-icon" src="data:image/png;base64,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"/> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_EDIT_TRANSLATION_HEADER')) . '</h1><div class="wrap">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1066 echo '<form method="post" id="edit-translations" action="admin-post.php">
1067 <p>
1068 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVE')) . '" class="button button-primary" data-action="save_gptranslate_record">
1069 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SAVEANDECLOSE')) . '" class="button button-primary" data-action="save_gptranslate_record_and_close">
1070 <input type="submit" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CANCEL')) . '" class="button button-primary" data-action="cancel_gptranslate_record">
1071 </p>
1072 <input type="hidden" name="languagetranslated" id="languagetranslated" value="' . esc_attr($record->languagetranslated) . '">
1073 <input type="hidden" name="action" id="form_action" value="save_gptranslate_record">
1074 <input type="hidden" name="id" value="' . (int) $record->id . '">
1075
1076 <table class="form-table">
1077 <tr>
1078 <th scope="row"><label for="pagelink" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</label></th>
1079 <td><input type="text" id="pagelink" name="pagelink" value="' . esc_attr($record->pagelink) . '" class="regular-text code"></td>
1080 </tr>' .
1081 $rewriteAliasRow .
1082 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</label></th><td>' . '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:5px;">' . ' ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($record->languageoriginal))) . '</td></tr>' . // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1083 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</label></th><td>' . '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:5px;">' . ' ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($record->languagetranslated))) . '</td></tr>' . // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1084 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED')) . '</label></th><td>' . wp_kses_post($pubIcon) . '</td></tr>' .
1085 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_DATE')) . '</label></th><td>' . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime( get_date_from_gmt($record->translate_date) ) ) ) . '</td></tr>' .
1086 '<tr><th scope="row"><label>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE')) . '</label></th><td><span class="gpt-label">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper(esc_attr($record->translation_engine)) . '_ENGINE')) . '</span></td></tr>
1087
1088 <tr>
1089 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS')) . '</label></th>
1090 <td>
1091 <div class="gptcard gptcard-default">
1092 <div class="gptcard-header">
1093 <div class="accordion-toggle">
1094 <div class="input-group">
1095 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
1096 <input type="text" name="search" value="" class="text_area">
1097 <button class="btn btn-primary" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
1098 <button class="btn btn-primary" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
1099 <select class="gpt-sort-select" data-role="sort-translations">
1100 <option value="length-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_DESC')) . '</option>
1101 <option value="alpha-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_ASC')) . '</option>
1102 <option value="alpha-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_DESC')) . '</option>
1103 <option value="length-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_ASC')) . '</option>
1104 </select>
1105 </div>
1106 </div>
1107 </div>
1108 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
1109 <button type="button" class="btn btn-success btn-adder" data-addtype="translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_TRANSLATION')) . '</button>
1110 <textarea name="translations_json" id="translations_json" hidden>' . esc_textarea(json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
1111 <div id="translations-container"></div>
1112 </div>
1113 </div>
1114 </td>
1115 </tr>
1116
1117 <tr class="alt_translations_' . (int)$opts['translate_altimages'] . '">
1118 <th scope="row"><label title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS_DESC')) . '">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALT_TRANSLATIONS')) . '</label></th>
1119 <td>
1120 <div class="gptcard gptcard-default">
1121 <div class="gptcard-header">
1122 <div class="accordion-toggle">
1123 <div class="input-group">
1124 <span class="gpt-label" aria-label="Filter">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '</span>
1125 <input type="text" name="search" value="" class="text_area">
1126 <button class="btn btn-primary btn-sm" data-role="search-translations" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>
1127 <button class="btn btn-primary btn-sm" data-role="reset-search" onclick="return false;">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
1128 <select class="gpt-sort-select" data-role="sort-translations">
1129 <option value="length-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_DESC')) . '</option>
1130 <option value="alpha-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_ASC')) . '</option>
1131 <option value="alpha-desc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_ALPHA_DESC')) . '</option>
1132 <option value="length-asc">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_SORT_LENGTH_ASC')) . '</option>
1133 </select>
1134 </div>
1135 </div>
1136 </div>
1137 <div class="gptcard-body gptcard-block ps-3 accordion-body accordion-inner">
1138 <button type="button" class="btn btn-success btn-adder" data-addtype="alt-translations">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ADD_ALT_TRANSLATION')) . '</button>
1139 <textarea name="alt_translations_json" id="alt_translations_json" hidden>' . esc_textarea(json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</textarea>
1140 <div id="alt-translations-container"></div>
1141 </div>
1142 </div>
1143 </td>
1144 </tr>
1145 </table>' .
1146 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nonce field is safe
1147 wp_nonce_field('gptranslate_save_record_action', '_gptranslate_nonce') .
1148 '</form>';
1149
1150 echo '<script>
1151 const initialTranslations = ' . (json_encode($translationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP) ?: '{}') . ';
1152 const initialAltTranslations = ' . (json_encode($altTranslationsArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP) ?: '{}') . ';
1153
1154 const PLG_GPTRANSLATE_ORIGINAL_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_ORIGINAL_TEXT')) . '";
1155 const PLG_GPTRANSLATE_TRANSLATED_TEXT = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_TEXT')) . '";
1156 const PLG_GPTRANSLATE_DELETE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '";
1157 const PLG_GPTRANSLATE_MOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MOVE')) . '";
1158 const PLG_GPTRANSLATE_REMOVE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_REMOVE')) . '";
1159 const PLG_GPTRANSLATE_SYNC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC')) . '";
1160 const PLG_GPTRANSLATE_SYNC_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_TITLE')) . '";
1161 const PLG_GPTRANSLATE_SYNC_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_DESC')) . '";
1162 const PLG_GPTRANSLATE_SYNC_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_COMPLETED')) . '";
1163 const PLG_GPTRANSLATE_SYNC_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_SYNC_ERROR')) . '";
1164 const gptServerSideLink = "' . esc_url_raw(rest_url('gptranslate/v1/request')) . '";
1165 </script>';
1166 } else {
1167 // FREE period
1168 // echo '<div class="notice notice-info is-dismissible"><p>You’re currently using the full version of GPTranslate – completely FREE during the initial launch period! Enjoy unlimited AI-powered translations and all PRO features at no cost. 🚀</p></div>';
1169
1170 // UPGRADE period
1171 echo '<div class="notice notice-warning is-dismissible"><p>⚠️ GPTranslate runs in FREE Mode with usage limits. To unlock unlimited translations and advanced features, upgrade to the <a href="https://gptranslate.storejextensions.org/" target="_blank"><strong>PRO version</strong></a>. Current FREE Plan: translate up to <strong>2500 words</strong>, read aloud up to <strong>100 words</strong> per page and crawl up to <strong>10 pages</strong>. Don’t lose AI power – <a href="https://gptranslate.storejextensions.org/" target="_blank">Upgrade Now</a>.</p></div>';
1172
1173 // List records
1174 echo '<h1><img class="gptranslate-plugin-icon" src="data:image/png;base64,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"/> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_HEADER')) . '</h1><div class="wrap">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1175
1176 // Filters section
1177 $opts = get_option( 'gptranslate_options', [] );
1178 $languageFilter = esc_attr( sanitize_text_field( wp_unslash( $_GET['language'] ?? '' ) ) );
1179 $languages = $opts['languages'] ?? [];
1180
1181 // Paginate records
1182 $records_per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 10;
1183 $valid_per_page_options = [5, 10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 9999999];
1184 if (!in_array($records_per_page, $valid_per_page_options)) {
1185 $records_per_page = 10;
1186 }
1187
1188 $searchFilter = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) );
1189 $engineFilter = sanitize_key( wp_unslash( $_GET['engine'] ?? '' ) );
1190 $publishedFilter = sanitize_key( wp_unslash( $_GET['published'] ?? '' ) );
1191 $exactMatchFilter = isset($_GET['exactmatch']) && $_GET['exactmatch'] == '1' ? true : false;
1192
1193 echo
1194 '<form method="get" class="form-filter-container">' .
1195 '<div class="left-filter-container">' .
1196 '<input type="text" name="s" id="search-input" value="' . (isset($_GET['s']) ? esc_attr(sanitize_text_field( wp_unslash($_GET['s']))) : '') . '" placeholder="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_SEARCH')) . '" />' .
1197 '<button type="button" class="button" onclick="this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_GO')) . '</button>' .
1198 '<button type="button" class="button" onclick="document.getElementById(\'search-input\').value=\'\'; this.form.submit();">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>' .
1199 '<label class="button"><input type="checkbox" name="exactmatch" value="1" ' . checked($exactMatchFilter, true, false) . ' title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_EXACT_MATCH')) . '"> ' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_EXACT_MATCH_LABEL')) . '</label>' .
1200 '</div>' .
1201 '<div class="right-filter-container">' .
1202 '<input type="hidden" name="_gptranslate_nonce" value="' . esc_attr(wp_create_nonce('gptranslate_filter_action')) . '" />' .
1203 '<select name="published">' .
1204 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_ALL')) . '</option>' .
1205 '<option value="1" ' . esc_html($this->isSelected(esc_attr((isset($_GET['published']) ? esc_attr(sanitize_key( wp_unslash($_GET['published']))) : '')), '1')) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</option>' .
1206 '<option value="0" ' . esc_html($this->isSelected(esc_attr((isset($_GET['published']) ? esc_attr(sanitize_key( wp_unslash($_GET['published']))) : '')), '0')) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '</option>' .
1207 '</select>' .
1208 '<select name="language">' .
1209 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</option>';
1210
1211
1212 // Loop languages
1213 foreach ($languages as $lang) {
1214 echo "<option value='" . esc_attr($lang) . "'" . esc_html($this->isSelected($languageFilter, $lang)) . ">" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($lang))) . "</option>";
1215 }
1216
1217 echo
1218 '</select>' .
1219 '<select name="engine">' .
1220 '<option value="">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE')) . '</option>' .
1221 '<option value="gtranslate" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'gtranslate')) . '>Google AI</option>' .
1222 '<option value="chatgpt" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'chatgpt')) . '>ChatGPT</option>' .
1223 '<option value="gemini" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'gemini')) . '>Gemini</option>' .
1224 '<option value="deepseek" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'deepseek')) . '>DeepSeek</option>' .
1225 '<option value="grok" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'grok')) . '>Grok</option>' .
1226 '<option value="googlecloud" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'googlecloud')) . '>Google Cloud</option>' .
1227 '<option value="claude" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'claude')) . '>Claude</option>' .
1228 '<option value="deepl" ' . esc_html($this->isSelected(sanitize_key(wp_unslash($_GET['engine'] ?? '')), 'deepl')) . '>DeepL</option>' .
1229 '</select>' .
1230 '<select name="per_page">' .
1231 '<option value="5" ' . esc_html($this->isSelected($records_per_page, 5)) . '>5</option>' .
1232 '<option value="10" ' . esc_html($this->isSelected($records_per_page, 10)) . '>10</option>' .
1233 '<option value="25" ' . esc_html($this->isSelected($records_per_page, 25)) . '>25</option>' .
1234 '<option value="50" ' . esc_html($this->isSelected($records_per_page, 50)) . '>50</option>' .
1235 '<option value="100" ' . esc_html($this->isSelected($records_per_page, 100)) . '>100</option>' .
1236 '<option value="200" ' . esc_html($this->isSelected($records_per_page, 200)) . '>200</option>' .
1237 '<option value="500" ' . esc_html($this->isSelected($records_per_page, 500)) . '>500</option>' .
1238 '<option value="1000" ' . esc_html($this->isSelected($records_per_page, 1000)) . '>1000</option>' .
1239 '<option value="2000" ' . esc_html($this->isSelected($records_per_page, 2000)) . '>2000</option>' .
1240 '<option value="5000" ' . esc_html($this->isSelected($records_per_page, 5000)) . '>5000</option>' .
1241 '<option value="10000" ' . esc_html($this->isSelected($records_per_page, 10000)) . '>10000</option>' .
1242 '<option value="9999999" ' . esc_html($this->isSelected($records_per_page, 9999999)) . '>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ALL')) . '</option>' .
1243 '</select>' .
1244 '<input type="submit" class="button" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_FILTER')) . '" />' .
1245 '</div>' .
1246 '<input type="hidden" name="page" value="gptranslate" />' .
1247 '</form>';
1248
1249 // Bottoni Import/Export
1250 echo '<div class="action-buttons-toolbar">';
1251 echo '<button class="button button-primary" id="bulk-delete-btn">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_DELETE')) . '</button>';
1252 echo '<button class="button button-primary" id="toggle-migration">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_BTNS')) . '</button>';
1253
1254 // Export CSV/XLIFF
1255 $exportFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_export_translations_xliff' : 'gptranslate_export_translations_csv';
1256 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" style="display:inline;margin-right:10px;">';
1257 if($exportFormat == 'gptranslate_export_translations_csv') {
1258 wp_nonce_field('gptranslate_export_csv', 'gptranslate_export_csv_nonce');
1259 } else {
1260 wp_nonce_field('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce');
1261 }
1262 echo '<input type="hidden" name="action" value="' . $exportFormat . '">';
1263 echo '<input type="submit" class="button button-primary" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_EXPORT_TRANSLATIONS')) . '">';
1264 echo '</form>';
1265
1266 // Import CSV/XLIFF
1267 $importFormat = (isset($opts['translations_export_format']) && $opts['translations_export_format'] == '.xliff') ? 'gptranslate_import_translations_xliff' : 'gptranslate_import_translations_csv';
1268 echo '<input type="button" class="button button-primary button-import" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_IMPORT_TRANSLATIONS')) . '">';
1269 echo '<form method="post" action="' . esc_attr(admin_url('admin-post.php')) . '" enctype="multipart/form-data" style="display:inline;">';
1270 if($importFormat == 'gptranslate_import_translations_csv') {
1271 wp_nonce_field('gptranslate_import_csv', 'gptranslate_import_csv_nonce');
1272 $acceptFormat = '.csv';
1273 } else {
1274 wp_nonce_field('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce');
1275 $acceptFormat = '.xliff';
1276 }
1277 echo '<input type="hidden" name="action" value="' . $importFormat . '">';
1278 echo '<input type="file" name="import_file" class="toggle-import hidden" accept="' . $acceptFormat . '" required>';
1279 echo '<input type="submit" class="button button-primary toggle-import hidden" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '">';
1280 echo '</form>';
1281
1282 echo '</div>';
1283
1284 echo '<div id="migraterow" class="hidden">
1285 <span class="input-group">
1286 <label for="migratetranslations_currentdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_PREVIOUS_DOMAIN')) . '</strong></label>
1287 <input type="text" class="form-control" id="migratetranslations_currentdomain" name="migratetranslations_currentdomain" value="">
1288 </span>
1289 <span class="input-group">
1290 <label for="migratetranslations_newdomain"><strong>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_NEW_DOMAIN')) . '</strong></label>
1291 <input type="text" class="form-control" id="migratetranslations_newdomain" name="migratetranslations_newdomain" value="">
1292 </span>
1293 <button class="button button-primary" id="migrationconfirm">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_MIGRATE_META_CONFIRM')) . '</button>
1294 <button class="button" id="migrationcancel">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_RESET')) . '</button>
1295 </div>
1296 <input type="button" class="button button-warning button-crawler" value="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '">
1297
1298 <script>
1299 const PLG_GPTRANSLATE_MIGRATION_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_SUCCESS')) . '";
1300 const PLG_GPTRANSLATE_MIGRATION_FAILED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_MIGRATION_FAILED')) . '";
1301 const PLG_GPTRANSLATE_UNKNOWN_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_UNKNOWN_ERROR')) . '";
1302 const PLG_GPTRANSLATE_NETWORK_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_NETWORK_ERROR')) . '";
1303 const PLG_GPTRANSLATE_BULK_DELETE_CONFIRM = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_CONFIRM')) . '";
1304 const PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SELECT_ONE')) . '";
1305 const PLG_GPTRANSLATE_BULK_DELETE_SUCCESS = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_SUCCESS')) . '";
1306 const PLG_GPTRANSLATE_BULK_DELETE_ERROR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_ERROR')) . '";
1307 const PLG_GPTRANSLATE_BULK_DELETE_NETWORK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_BULK_DELETE_NETWORK')) . '";
1308 const PLG_GPTRANSLATE_CRAWLER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER')) . '";
1309 const PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_DIALOG_TITLE')) . '";
1310 const PLG_GPTRANSLATE_CRAWLER_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TARGET_LINK')) . '";
1311 const PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK')) . '";
1312 const PLG_GPTRANSLATE_CRAWLER_START = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START')) . '";
1313 const PLG_GPTRANSLATE_CRAWLER_START_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_START_DESC')) . '";
1314 const PLG_GPTRANSLATE_CRAWLER_STARTED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTED')) . '";
1315 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING')) . '";
1316 const PLG_GPTRANSLATE_CRAWLER_FOOTER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_FOOTER')) . '";
1317 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE')) . '";
1318 const PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED')) . '";
1319 const PLG_GPTRANSLATE_CRAWLER_STOP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP')) . '";
1320 const PLG_GPTRANSLATE_CRAWLER_STOP_DESC = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOP_DESC')) . '";
1321 const PLG_GPTRANSLATE_CRAWLER_STARTING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STARTING')) . '";
1322 const PLG_GPTRANSLATE_CRAWLER_STOPPING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPING')) . '";
1323 const PLG_GPTRANSLATE_CRAWLER_STOPPED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_STOPPED')) . '";
1324 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
1325 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
1326 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
1327 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
1328 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
1329 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
1330 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
1331 const PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION')) . '";
1332 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL')) . '";
1333 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL')) . '";
1334 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES')) . '";
1335 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED')) . '";
1336 const PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED')) . '";
1337 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION')) . '";
1338 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER')) . '";
1339 var gptranslateBaseCrawlerHome = "' . esc_js( trailingslashit( get_site_url() ) ) . '";
1340 var gptranslateSitemapUrl = "' . esc_js( rest_url( 'gptranslate/v1/sitemap.xml' ) ) . '";
1341 var gptranslateDefaultLanguage = "' . $opts['language'] . '";
1342 var gptranslateCrawlerTimeout = "' . (isset($opts['crawler_timeout']) ? $opts['crawler_timeout'] : '30') . '";
1343 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($opts['crawler_exclusions']) ? $opts['crawler_exclusions'] : ''))), ',')) . '";
1344 var gptranslateCrawlerSkipTranslated = ' . (int)($opts['crawler_skip_translated'] ?? 0) . ';
1345 var gptSubfolderInstallation = ' . (int)($opts['subfolder_installation'] ?? 0) . ';
1346 var gptranslateRewriteLanguageUrl = ' . (int)$opts['rewrite_language_url'] . ';
1347 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($opts['omit_prefix_original_language']) ? (int)$opts['omit_prefix_original_language'] : 0) . ';
1348 var gptVersionNumeric = ' . 0 . ';
1349 var gptranslateSitemapAvailableLanguages = ' . wp_json_encode(gptranslate_get_available_languages()) . ';
1350 </script>';
1351
1352
1353 echo '<table class="widefat fixed striped">';
1354 echo '<thead><tr>';
1355 echo '<th style="width: 1%"><input class="form-check-input" autocomplete="off" type="checkbox" id="checkall"></th>';
1356 echo '<th style="width: 2%;;white-space:nowrap">ID</th>';
1357 if($opts['rewrite_language_alias'] == 1) {
1358 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
1359 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</th>';
1360 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
1361 } else {
1362 echo '<th style="width: 25%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
1363 echo '<th style="width: 20%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
1364 }
1365 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</th>';
1366 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</th>';
1367 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</th>';
1368 echo '<th style="width:5%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_ENGINE')) . '</th>';
1369 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DATE')) . '</th>';
1370 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ACTIONS')) . '</th>';
1371 echo '</tr></thead>';
1372
1373 echo '<tbody>';
1374
1375 $current_page = isset($_GET['paged']) && is_numeric($_GET['paged']) ? (int)$_GET['paged'] : 1;
1376 $offset = ($current_page - 1) * $records_per_page;
1377 $sql_count = "SELECT COUNT(*) FROM {$this->table_name} WHERE 1=1";
1378
1379 // Add dynamic filters
1380 if (!empty($searchFilter)) {
1381 if ($exactMatchFilter) {
1382 // Ricerca esatta
1383 $sql_count .= " AND (pagelink = '" . esc_sql($searchFilter) . "' OR translated_alias = '" . esc_sql($searchFilter) . "')";
1384 } else {
1385 // Ricerca LIKE (comportamento attuale)
1386 $sql_count .= " AND (pagelink LIKE '%" . esc_sql($searchFilter) . "%' OR translated_alias LIKE '%" . esc_sql($searchFilter) . "%' OR translations LIKE '%" . esc_sql($searchFilter) . "%'" . " OR alt_translations LIKE '%" . esc_sql($searchFilter) . "%'" . ")";
1387 }
1388 }
1389 if ($publishedFilter !== '') {
1390 $sql_count .= " AND published = '" . esc_sql($publishedFilter) . "'";
1391 }
1392 if (!empty($languageFilter)) {
1393 $sql_count .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
1394 }
1395 if (!empty($engineFilter)) {
1396 $sql_count .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
1397 }
1398 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1399 $total_records = $wpdb->get_var($sql_count); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1400 $total_pages = ceil($total_records / $records_per_page);
1401
1402 // Load records count with filtering
1403 $sql_data = "SELECT * FROM {$this->table_name} WHERE 1=1";
1404
1405 // Add dynamic filters
1406 if (!empty($searchFilter)) {
1407 if ($exactMatchFilter) {
1408 // Ricerca esatta
1409 $sql_data .= " AND (pagelink = '" . esc_sql($searchFilter) . "' OR translated_alias = '" . esc_sql($searchFilter) . "')";
1410 } else {
1411 // Ricerca LIKE (comportamento attuale)
1412 $sql_data .= " AND (pagelink LIKE '%" . esc_sql($searchFilter) . "%' OR translated_alias LIKE '%" . esc_sql($searchFilter) . "%' OR translations LIKE '%" . esc_sql($searchFilter) . "%'" . " OR alt_translations LIKE '%" . esc_sql($searchFilter) . "%'" . ")";
1413 }
1414 }
1415 if ($publishedFilter !== '') {
1416 $sql_data .= " AND published = '" . esc_sql($publishedFilter) . "'";
1417 }
1418 if (!empty($languageFilter)) {
1419 $sql_data .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
1420 }
1421 if (!empty($engineFilter)) {
1422 $sql_data .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
1423 }
1424 $sql_data .= " ORDER BY translate_date DESC LIMIT $records_per_page OFFSET $offset";
1425
1426 // Load records with filtering and pagination
1427 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1428 $records = $wpdb->get_results($sql_data); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1429
1430 // Message for no translations
1431 if (!count($records) && stripos($sql_data, "AND") === false) {
1432 echo '<div class="notice notice-success is-dismissible" id="notranslations-notice">';
1433 echo '<p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO_TRANSLATIONS')) . '</p>';
1434 echo '</div>';
1435 ?>
1436 <div id="gptranslate-zero-state" class="gptranslate-zero-state">
1437 <div class="gptranslate-zero-icon">
1438 <svg fill="#222222" height="96px" width="96px" version="1.1" id="XMLID_275_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" xml:space="preserve">
1439 <g id="language">
1440 <g>
1441 <path d="M12,24C5.4,24,0,18.6,0,12S5.4,0,12,0s12,5.4,12,12S18.6,24,12,24z M9.5,17c0.6,3.1,1.7,5,2.5,5s1.9-1.9,2.5-5H9.5z
1442 M16.6,17c-0.3,1.7-0.8,3.3-1.4,4.5c2.3-0.8,4.3-2.4,5.5-4.5H16.6z M3.3,17c1.2,2.1,3.2,3.7,5.5,4.5c-0.6-1.2-1.1-2.8-1.4-4.5H3.3
1443 z M16.9,15h4.7c0.2-0.9,0.4-2,0.4-3s-0.2-2.1-0.5-3h-4.7c0.2,1,0.2,2,0.2,3S17,14,16.9,15z M9.2,15h5.7c0.1-0.9,0.2-1.9,0.2-3
1444 S15,9.9,14.9,9H9.2C9.1,9.9,9,10.9,9,12C9,13.1,9.1,14.1,9.2,15z M2.5,15h4.7c-0.1-1-0.1-2-0.1-3s0-2,0.1-3H2.5C2.2,9.9,2,11,2,12
1445 S2.2,14.1,2.5,15z M16.6,7h4.1c-1.2-2.1-3.2-3.7-5.5-4.5C15.8,3.7,16.3,5.3,16.6,7z M9.5,7h5.1c-0.6-3.1-1.7-5-2.5-5
1446 C11.3,2,10.1,3.9,9.5,7z M3.3,7h4.1c0.3-1.7,0.8-3.3,1.4-4.5C6.5,3.3,4.6,4.9,3.3,7z"/>
1447 </g>
1448 </g>
1449 </svg>
1450 </div>
1451
1452 <div class="gptranslate-zero-title">
1453 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_TITLE')); ?>
1454 </div>
1455
1456 <p class="gptranslate-zero-desc">
1457 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_DESC')); ?>
1458 </p>
1459
1460 <button id="gptranslate-start-crawler" class="button button-primary button-hero">
1461 <span class="dashicons-before dashicons-translation" aria-hidden="true"></span>
1462 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_BUTTON')); ?>
1463 </button>
1464
1465 <p class="gptranslate-zero-hint">
1466 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_HINT')); ?>
1467 </p>
1468 </div>
1469 <?php
1470 }
1471
1472 foreach ( $records as $r ) {
1473 // Build a short summary of the translations
1474 $tr = json_decode( $r->translations, true );
1475 if ( is_array( $tr ) ) {
1476 $pairs = array_map(
1477 function( $k, $v ) {
1478 return esc_html( $k ) . '' . esc_html( $v );
1479 },
1480 array_keys( $tr ),
1481 array_values( $tr )
1482 );
1483 $short = implode( ', ', $pairs );
1484 $short = mb_substr( $short, 0, 80 ) . ( mb_strlen( $short ) > 80 ? '' : '' );
1485 } else {
1486 $short = '';
1487 }
1488
1489 // Escape all output
1490 $id = (int) $r->id;
1491 $link = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=edit&edit={$id}" ), 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
1492 $deleteLink = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=delete_translation&translation_id={$id}" ), 'gptranslate_delete_' . $id, '_gptranslate_nonce' );
1493 $origLang = esc_html( strtoupper( $r->languageoriginal ) );
1494 $transLang = esc_html( strtoupper( $r->languagetranslated ) );
1495 $pub = $r->published ? esc_html($this->loadTranslations('PLG_GPTRANSLATE_YES')) : esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO'));
1496 $engine = esc_html( $r->translation_engine );
1497 $date = esc_html( $r->translate_date );
1498
1499 $langOriginal = esc_attr($r->languageoriginal);
1500
1501 // Path relativo o assoluto all'immagine della bandiera
1502 $flagUrlOriginal = plugins_url('flags/svg/' . $r->languageoriginal . '.svg', __FILE__);
1503 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1504 $flagUrlTranslated = plugins_url('flags/svg/' . $r->languagetranslated . '.svg', __FILE__);
1505 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1506
1507 // Alternative flags check for list view
1508 $altFlagsOpts = isset($opts['alt_flags']) && is_array($opts['alt_flags']) ? $opts['alt_flags'] : [];
1509 $altFlagMap = [
1510 'en' => ['usa' => 'en-us', 'canada' => 'en-ca', 'ireland' => 'en-ie'],
1511 'pt' => ['brazil' => 'pt-br'],
1512 'es' => ['mexico' => 'es-mx', 'argentina' => 'es-ar', 'colombia' => 'es-co'],
1513 'fr' => ['quebec' => 'fr-qc'],
1514 'zh' => ['taiwan' => 'zh-TW'],
1515 'zt' => ['hongkong' => 'zh-HK'],
1516 'de' => ['austria' => 'de-at'],
1517 ];
1518 foreach ($altFlagMap as $langCode => $variants) {
1519 foreach ($variants as $country => $flagFile) {
1520 if (in_array($country, $altFlagsOpts)) {
1521 if ($r->languageoriginal === $langCode) {
1522 $flagUrlOriginal = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1523 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1524 }
1525 if ($r->languagetranslated === $langCode) {
1526 $flagUrlTranslated = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1527 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1528 }
1529 break;
1530 }
1531 }
1532 }
1533
1534 $togglePublishedUrl = wp_nonce_url(
1535 admin_url("admin.php?page=gptranslate&action=toggle_published&translation_id={$id}"),
1536 'gptranslate_toggle_' . $id,
1537 '_gptranslate_nonce'
1538 );
1539
1540 $pubIcon = $r->published ? '<img src="' . plugins_url('assets/images/published.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . '">' // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1541 : '<img src="' . plugins_url('assets/images/unpublished.png', __FILE__) . '" alt="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UTRANSLATIONS_SHORT_CHART')) . '" title="' . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . '">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1542
1543
1544 $pub = $r->published ? "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-published' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . "'>" . $pubIcon . "</a>"
1545 : "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-unpublished' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . "'>" . $pubIcon . "</a>";
1546
1547 $local_date = get_date_from_gmt($date);
1548
1549 echo '<tr>';
1550 echo "<td style='width: 1%'><input class='form-check-input' autocomplete='off' type='checkbox' id='cb0' name='gptid[]' value='" . esc_attr($r->id) . "'></td>";
1551 echo "<td style='width: 2%;white-space:nowrap'>". esc_html($r->id) . "</td>";
1552 echo "<td><a href='" . esc_attr( $link ) . "'><span class='icon-style' aria-hidden='true'>📝</span>" . esc_html( $r->pagelink ) . "</a><a class='doublesize-icon' href='" . esc_attr( $r->pagelink ) . "' target='_blank'>↗</a></td>";
1553 if($opts['rewrite_language_alias'] == 1) {
1554 echo $r->translated_alias ? "<td><a href='" . esc_attr( $r->translated_alias ) . "'>" . esc_html( $r->translated_alias ) . "</a><a class='doublesize-icon' href='" . esc_attr( $r->translated_alias ) . "' target='_blank'>↗</a></td>" : '<td>-</td>';
1555 }
1556 echo "<td>" . esc_html($short) . "</td>";
1557 echo "<td>" . wp_kses_post($flagOriginal) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($langOriginal))) . "</td>";
1558 echo "<td>" . wp_kses_post($flagTranslated) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper(esc_attr($r->languagetranslated)))) . "</td>";
1559 echo "<td>" . wp_kses_post($pub) . "</td>";
1560 echo "<td><span class='gpt-label'>" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper($engine) . '_ENGINE')) . "</span></td>";
1561 echo "<td>" . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime($local_date)) ) . "</td>";
1562 echo "<td><a href='" . esc_attr($link) . "'>" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_EDIT')) . "</a> | <a href='" . esc_attr($deleteLink) . "' onclick=\"return confirm('" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETE_CONFIRM')) . "');\">" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATION_DELETE')) . "</a></td>";
1563 echo '</tr>';
1564 }
1565 echo '</tbody>';
1566 echo '</table>';
1567
1568 if ($total_pages > 1) {
1569 echo '<div class="tablenav"><div class="tablenav-pages">';
1570 for ($i = 1; $i <= $total_pages; $i++) {
1571 $url = add_query_arg(array_merge($_GET, ['paged' => $i]), admin_url('admin.php'));
1572 $class = ($i == $current_page) ? "class='current-page button'" : "class='button'";
1573 echo "<a " . wp_kses_post($class) . " href='" . esc_url($url) . "'>" . esc_html($i) . "</a> ";
1574 }
1575 echo '</div></div>';
1576 }
1577 }
1578
1579 echo '</div>';
1580 echo '</div>';
1581 }
1582
1583 /**
1584 * Load language file and translations
1585 * @return array
1586 */
1587 public function loadTranslations($key) {
1588 // Text translations
1589 static $adminLanguageStrings = null;
1590
1591 if($adminLanguageStrings === null) {
1592 // Use GPTRANSLATE_CURRENT_LANG if defined (server-side language), fallback to blog language
1593 $langTag = defined('GPTRANSLATE_CURRENT_LANG') ? GPTRANSLATE_CURRENT_LANG : get_bloginfo('language');
1594
1595 // Normalize language tag: "it" -> "it-IT"
1596 if(strlen($langTag) == 2) {
1597 $langTag = $langTag . '-' . strtoupper($langTag);
1598 }
1599
1600 // Try to load the specific language file
1601 $adminLanguageFile = dirname(__FILE__) . "/language/" . $langTag . "/gptranslate.ini";
1602 if(!file_exists($adminLanguageFile)) {
1603 // Fallback to English if specific language file doesn't exist
1604 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
1605 }
1606
1607 $adminLanguageStrings = array();
1608 if(file_exists($adminLanguageFile)) {
1609 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
1610 }
1611 }
1612
1613 if(array_key_exists($key, $adminLanguageStrings)) {
1614 return $adminLanguageStrings[$key];
1615 }
1616
1617 return $key;
1618 }
1619
1620 /**
1621 * Load language file and translations
1622 * @return array
1623 */
1624 public static function loadTranslation($key) {
1625 // Text translations
1626 static $adminLanguageStrings = null;
1627
1628 if($adminLanguageStrings === null) {
1629 // Use GPTRANSLATE_CURRENT_LANG if defined (server-side language), fallback to blog language
1630 $langTag = defined('GPTRANSLATE_CURRENT_LANG') ? GPTRANSLATE_CURRENT_LANG : get_bloginfo('language');
1631
1632 // Normalize language tag: "it" -> "it-IT"
1633 if(strlen($langTag) == 2) {
1634 $langTag = $langTag . '-' . strtoupper($langTag);
1635 }
1636
1637 // Try to load the specific language file
1638 $adminLanguageFile = dirname(__FILE__) . "/language/" . $langTag . "/gptranslate.ini";
1639 if(!file_exists($adminLanguageFile)) {
1640 // Fallback to English if specific language file doesn't exist
1641 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
1642 }
1643
1644 $adminLanguageStrings = array();
1645 if(file_exists($adminLanguageFile)) {
1646 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
1647 }
1648 }
1649
1650 if(array_key_exists($key, $adminLanguageStrings)) {
1651 return $adminLanguageStrings[$key];
1652 }
1653
1654 return $key;
1655 }
1656
1657 /**
1658 * Save translation record
1659 *
1660 * @access public
1661 */
1662 public function save_record() {
1663 global $wpdb;
1664
1665 // Retrieve and sanitize basic inputs
1666 $id = isset ( $_POST ['id'] ) ? intval ( $_POST ['id'] ) : 0;
1667 $formAction = isset ( $_POST ['action'] ) ? sanitize_key ( $_POST ['action'] ) : '';
1668
1669 if ( !isset($_POST['_gptranslate_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_gptranslate_nonce'])), 'gptranslate_save_record_action') ) {
1670 wp_die(esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'gptranslate');
1671 }
1672
1673 // Handle cancel action
1674 if ($formAction === 'cancel_gptranslate_record') {
1675 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
1676 exit ();
1677 }
1678
1679 // Sanitize pagelink
1680 $pagelink = isset ( $_POST ['pagelink'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['pagelink'] ) ) : '';
1681
1682 // Sanitize translated_alias
1683 $translatedAlias = isset ( $_POST ['translated_alias'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['translated_alias'] ) ) : '';
1684
1685 // Process and sanitize translations JSON
1686 $raw_translations = filter_input( INPUT_POST, 'translations_json', FILTER_UNSAFE_RAW );
1687 $raw_translations = is_string($raw_translations) ? $raw_translations : '[]';
1688
1689 $decoded_translations = json_decode ( $raw_translations, true );
1690 if (! is_array ( $decoded_translations )) {
1691 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
1692 'response' => 400
1693 ] );
1694 }
1695 $clean_translations = $decoded_translations;
1696 $sanitized_translations_json = wp_json_encode ( $clean_translations );
1697
1698 // Process and sanitize alternative translations JSON
1699 $raw_alt = filter_input( INPUT_POST, 'alt_translations_json', FILTER_UNSAFE_RAW );
1700 $raw_alt = is_string($raw_translations) ? $raw_alt : '[]';
1701
1702 $decoded_alt = json_decode ( $raw_alt, true );
1703 if (! is_array ( $decoded_alt )) {
1704 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_ALTTRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
1705 'response' => 400
1706 ] );
1707 }
1708 $clean_alt = $decoded_alt;
1709 $sanitized_alt_json = wp_json_encode ( $clean_alt );
1710
1711 // Update database record
1712 $wpdb->update ( $this->table_name, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1713 'pagelink' => $pagelink,
1714 'translated_alias' => $translatedAlias,
1715 'translations' => $sanitized_translations_json,
1716 'alt_translations' => $sanitized_alt_json
1717 ], [
1718 'id' => $id
1719 ], [
1720 '%s',
1721 '%s',
1722 '%s'
1723 ], [
1724 '%d'
1725 ] );
1726
1727 // Redirect based on action
1728 if ($formAction === 'save_gptranslate_record_and_close') {
1729 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
1730 } else {
1731 $url = admin_url( 'admin.php?page=gptranslate&action=edit&edit=' . $id );
1732 $url = wp_nonce_url( $url, 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
1733 wp_redirect( html_entity_decode( $url ) );
1734 }
1735 exit ();
1736 }
1737
1738 public function gptranslate_handle_deletion() {
1739 // 1) Verify nonce
1740 $id = isset( $_GET['translation_id'] ) ? intval( $_GET['translation_id'] ) : 0;
1741 $nonce = isset( $_GET['_gptranslate_nonce'] ) ? wp_unslash( $_GET['_gptranslate_nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1742
1743 if ( ! wp_verify_nonce( $nonce, 'gptranslate_delete_' . $id ) ) {
1744 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
1745 }
1746
1747 // 2) Delete the row
1748 global $wpdb;
1749 $table = $wpdb->prefix . 'gptranslate';
1750 $deleted = $wpdb->delete( $table, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1751
1752 // 3) Redirect back with a message
1753 $redirect_url = add_query_arg(
1754 [
1755 'page' => 'gptranslate',
1756 'deleted' => $deleted ? '1' : '0',
1757 ],
1758 admin_url( 'admin.php' )
1759 );
1760 wp_redirect( $redirect_url );
1761 exit;
1762 }
1763
1764
1765 /**
1766 * Add main app frontend script
1767 *
1768 * @access public
1769 */
1770 public function enqueue_frontend_scripts() {
1771 add_filter('script_loader_tag', function($tag, $handle) {
1772 if ($handle === 'gptranslate-responsivevoice') {
1773 return str_replace('<script ', '<script defer ', $tag);
1774 }
1775
1776 if ($handle === 'gptranslate-jsonrepair') {
1777 return str_replace('<script ', '<script type="module" ', $tag);
1778 }
1779
1780 if ($handle === 'gptranslate-bstoast') {
1781 return str_replace('<script ', '<script type="module" ', $tag);
1782 }
1783 if ($handle === 'gptranslate-main') {
1784 // 1) prendi i valori raw
1785 $raw_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
1786 $raw_host = isset($_SERVER['HTTP_HOST']) ? wp_unslash($_SERVER['HTTP_HOST']) : '';
1787
1788 // 2) unslash WP
1789 $unslashed_uri = wp_unslash ( $raw_uri );
1790 $unslashed_host = wp_unslash ( $raw_host );
1791
1792 // 3) sanitizza URI come URL
1793 $orig_url = esc_url_raw ( $unslashed_uri );
1794 // - accetta path e query, rimuove caratteri pericolosi
1795
1796 // 4) sanitizza host
1797 // a) rimuovi tag e control chars
1798 $host_clean = sanitize_text_field ( $unslashed_host );
1799 // b) mantieni solo [a-z0-9.-] per sicurezza
1800 $orig_domain = preg_replace ( '/[^a-z0-9.-]/i', '', $host_clean );
1801
1802 // 5) infine escape per attributo HTML
1803 return str_replace ( '<script ', sprintf ( '<script type="module" data-gt-orig-url="%s" data-gt-orig-domain="%s" data-gt-widget-id="1" ', esc_attr ( $orig_url ), esc_attr ( $orig_domain ) ), $tag );
1804 }
1805
1806 return $tag;
1807 }, 10, 2);
1808
1809 $settings = get_option("gptranslate_options");
1810
1811 // Excluded languages check
1812 $excludedLangs = isset($settings['excluded_languages']) ? (array) $settings['excluded_languages'] : [];
1813 if (!empty($excludedLangs) && defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
1814 if (in_array(GPTRANSLATE_CURRENT_LANG, $excludedLangs, true)) {
1815 return;
1816 }
1817 }
1818
1819 // Move default language to the first one in the list
1820 if($settings ['default_language_first']) {
1821 if(!isset($settings ['languages'])) {
1822 $settings ['languages'] = array_map ( 'strtolower', [
1823 'AF',
1824 'SQ',
1825 'AM',
1826 'AR',
1827 'HY',
1828 'AZ',
1829 'EU',
1830 'BE',
1831 'BN',
1832 'BS',
1833 'BG',
1834 'CA',
1835 'CEB',
1836 'NY',
1837 'ZH',
1838 'CO',
1839 'HR',
1840 'CS',
1841 'DA',
1842 'NL',
1843 'EN',
1844 'EO',
1845 'ET',
1846 'TL',
1847 'FI',
1848 'FR',
1849 'FY',
1850 'GL',
1851 'KA',
1852 'DE',
1853 'EL',
1854 'GU',
1855 'HT',
1856 'HA',
1857 'HAW',
1858 'IW',
1859 'HI',
1860 'HMN',
1861 'HU',
1862 'IS',
1863 'IG',
1864 'ID',
1865 'GA',
1866 'IT',
1867 'JA',
1868 'JW',
1869 'KN',
1870 'KK',
1871 'KM',
1872 'KO',
1873 'KU',
1874 'KY',
1875 'LO',
1876 'LA',
1877 'LV',
1878 'LT',
1879 'LB',
1880 'MK',
1881 'MG',
1882 'MS',
1883 'ML',
1884 'MT',
1885 'MI',
1886 'MR',
1887 'MN',
1888 'MY',
1889 'NE',
1890 'NO',
1891 'PS',
1892 'FA',
1893 'PL',
1894 'PT',
1895 'PA',
1896 'RO',
1897 'RU',
1898 'SM',
1899 'GD',
1900 'SR',
1901 'ST',
1902 'SN',
1903 'SD',
1904 'SI',
1905 'SK',
1906 'SL',
1907 'SO',
1908 'ES',
1909 'SU',
1910 'SW',
1911 'SV',
1912 'TG',
1913 'TA',
1914 'TE',
1915 'TH',
1916 'TR',
1917 'UK',
1918 'UR',
1919 'UZ',
1920 'VI',
1921 'CY',
1922 'XH',
1923 'YI',
1924 'YO',
1925 'ZU',
1926 'ZT'
1927 ] );
1928 }
1929 $defaultLanguageKeyIndex = array_search($settings ['language'], $settings ['languages']);
1930 if ($defaultLanguageKeyIndex !== false) {
1931 // Remove the 'de' element from its current position
1932 $defaultLanguage = $settings ['languages'][$defaultLanguageKeyIndex];
1933 unset($settings ['languages'][$defaultLanguageKeyIndex]);
1934
1935 // Re-index the array to maintain numerical indexes
1936 $settings ['languages'] = array_values($settings ['languages']);
1937
1938 // Add 'de' to the beginning of the array
1939 array_unshift($settings ['languages'], $defaultLanguage);
1940 }
1941 }
1942
1943 // build alt_flags array
1944 $alt_flags = array ();
1945 $raw_alt_flags = isset($settings ['alt_flags']) ? $settings ['alt_flags'] : [];
1946 foreach ( $raw_alt_flags as $country ) {
1947 if ($country == 'usa' || $country == 'canada' || $country == 'ireland')
1948 $alt_flags ['en'] = $country;
1949 elseif ($country == 'brazil')
1950 $alt_flags ['pt'] = $country;
1951 elseif ($country == 'mexico' or $country == 'argentina' or $country == 'colombia')
1952 $alt_flags ['es'] = $country;
1953 elseif ($country == 'quebec')
1954 $alt_flags ['fr'] = $country;
1955 elseif ($country == 'taiwan')
1956 $alt_flags ['zh'] = $country;
1957 elseif ($country == 'hongkong')
1958 $alt_flags ['zt'] = $country;
1959 elseif ($country == 'austria')
1960 $alt_flags ['de'] = $country;
1961 }
1962
1963 // Build float position variables
1964 $float_position = $settings ['float_position'];
1965 if($float_position != 'inline'){
1966 list ( $switcher_vertical_position, $switcher_horizontal_position ) = explode ( '-', $float_position );
1967 } else {
1968 list ( $switcher_vertical_position, $switcher_horizontal_position ) = ['inline', 'inline'];
1969 }
1970
1971 // Set local flags path
1972 $flagsPath = trailingslashit(plugins_url('flags', __FILE__));
1973
1974 // Ensure a default array value
1975 if(!isset($settings['realtime_translations_retrigger_events'])) {
1976 $settings['realtime_translations_retrigger_events'] = ['click'];
1977 $settings['realtime_translations_retrigger_events_delay'] = 200;
1978 }
1979 if(!isset ( $settings ['realtime_translations_retrigger_force_google'] )) {
1980 $settings ['realtime_translations_retrigger_force_google'] = 0;
1981 }
1982 if(!isset ( $settings ['realtime_translations_retrigger_do_not_revert'] )) {
1983 $settings ['realtime_translations_retrigger_do_not_revert'] = 0;
1984 }
1985 if(!isset ( $settings ['translate_srcimages'] )) {
1986 $settings ['translate_srcimages'] = 0;
1987 }
1988 if(!isset ( $settings ['translate_srciframes'] )) {
1989 $settings ['translate_srciframes'] = 0;
1990 }
1991 if(!isset ( $settings ['translate_srcvideos'] )) {
1992 $settings ['translate_srcvideos'] = 0;
1993 }
1994 if(!isset ( $settings ['css_selector_realtime_translations_retrigger'] )) {
1995 $settings ['css_selector_realtime_translations_retrigger'] = '';
1996 }
1997 if(!isset($settings ['serverside_translations_language_switching_mode'])) {
1998 $settings ['serverside_translations_language_switching_mode'] = 'url';
1999 }
2000 if(!isset($settings ['excluded_alias_slugs'])) {
2001 $settings ['excluded_alias_slugs'] = '';
2002 }
2003 if(!isset($settings ['popup_shadow'])) {
2004 $settings ['popup_shadow'] = 1;
2005 }
2006 if(!isset($settings ['wrap_excluded_words'])) {
2007 $settings ['wrap_excluded_words'] = 1;
2008 }
2009 if(!isset($settings ['transliterate_urls'])) {
2010 $settings ['transliterate_urls'] = 0;
2011 }
2012 if(!isset($settings ['rewrite_form_actions'])) {
2013 $settings ['rewrite_form_actions'] = 0;
2014 }
2015 if(!isset($settings ['translate_iframe_locale'])) {
2016 $settings ['translate_iframe_locale'] = 0;
2017 }
2018
2019 wp_register_script('gptranslate-main-inline', '', [], $this->version, true);
2020 wp_enqueue_script('gptranslate-main-inline');
2021
2022 // Example: $settings is an array like in Joomla
2023 $base64Encode = 'base' . 64 . '_encode';
2024 $key = $settings['chatgpt_apikey'];
2025 $key = strrev($key);
2026 $secret = 'gptranslate';
2027 $out = '';
2028 for ($i = 0; $i < strlen($key); $i++) {
2029 $out .= chr(ord($key[$i]) ^ ord($secret[$i % strlen($secret)]));
2030 }
2031 $encoded = $base64Encode($out);
2032
2033 $ajaxEndpoint = esc_url_raw(rest_url('gptranslate/v1/request'));
2034
2035 $inlineScript = 'var gptServerSideLink = "' . esc_js($ajaxEndpoint) . '";';
2036 if (!empty($settings['lightweight_ajax_endpoint'])) {
2037 $lightweightEndpoint = esc_url_raw(plugin_dir_url(__FILE__) . 'ajax-handler.php');
2038 $inlineScript .= '
2039 var gptServerSideLightLink = "' . esc_js($lightweightEndpoint) . '";';
2040 }
2041 $inlineScript .= '
2042 var gptApiKey = "' . esc_js(gptranslate_get_api_secret()) . '";
2043 var gptAjaxSecret = "' . esc_js(gptranslate_get_api_secret()) . '";
2044 var gptRestNonce = "' . esc_js(wp_create_nonce('wp_rest')) . '";
2045 var gptLiveSite = "' . esc_js(get_site_url()) . '";
2046 var gptStorage = ' . ($settings['storage_type'] === 'session' ? 'window.sessionStorage' : 'window.localStorage') . ';
2047 var gptMaxTranslationsPerRequest = ' . (int)$settings['max_translations_per_request'] . ';
2048 var maxCharactersPerRequest = ' . (int)$settings['max_characters_per_request'] . ';
2049 var gptRewriteLanguageUrl = ' . (int)$settings['rewrite_language_url'] . ';
2050 var gptOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
2051 var gptExcludedAliasSlugs = "' . esc_js(rtrim($settings['excluded_alias_slugs'], ', ')) . '";
2052 var gptRewriteLanguageAlias = ' . (int)$settings['rewrite_language_alias'] . ';
2053 var gptRewriteLanguageAliasOriginalLanguage = ' . (int)$settings['rewrite_language_alias_original_language'] . ';
2054 var gptAutoSetLanguageDirection = ' . (int)$settings['auto_set_language_direction'] . ';
2055 var gptServersideTranslations = ' . (int)$settings['serverside_translations'] . ';
2056 var gptServersideTranslationsLanguageSwitchingMode = "' . $settings ['serverside_translations_language_switching_mode'] . '";
2057 var gptServersideTranslationsMatchquotes = ' . (int)$settings['serverside_translations_matchquotes'] . ';
2058 var gptRewritePageLinks = ' . (int)$settings['rewrite_page_links'] . ';
2059 var gptRewritePageLinksExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($settings['rewrite_page_links_exclusions']) ? $settings['rewrite_page_links_exclusions'] : ''))), ',')) . '";
2060 var gptRewriteFormActions = ' . (int)$settings['rewrite_form_actions'] . ';
2061 var gptTransliterateUrls = ' . (int)$settings ['transliterate_urls'] . ';
2062 var gptTranslateMetadata = ' . (int)$settings['translate_metadata'] . ';
2063 var gptTranslatePlaceholders = ' . (int)$settings['translate_placeholders'] . ';
2064 var gptTranslateAltImages = ' . (int)$settings['translate_altimages'] . ';
2065 var chatgptClassesAltimagesExcluded = "' . esc_js(str_ireplace('"', '', $settings['css_selector_classes_translate_altimages_excluded'])) . '";
2066 var gptTranslateSrcImages = ' . (int)$settings['translate_srcimages'] . ';
2067 var gptTranslateSrcIframes = ' . (int)$settings['translate_srciframes'] . ';
2068 var gptTranslateSrcVideos = ' . (int)$settings['translate_srcvideos'] . ';
2069 var gptTranslateIframeLocale = ' . (int)$settings['translate_iframe_locale'] . ';
2070 var gptTranslateTitles = ' . (int)$settings['translate_titles'] . ';
2071 var gptTranslateValues = ' . (int)$settings['translate_values'] . ';
2072 var gptMetadataChosenEngine = ' . (isset($settings['metadata_chosen_engine']) ? (int)$settings['metadata_chosen_engine'] : 0) . ';
2073 var chatgptMetadataWordsLeafnodesExcluded = "' . esc_js(rtrim($settings['metadata_words_leafnodes_excluded'], ', ')) . '";
2074 var gptSetHtmlLang = ' . (int)$settings['set_html_lang'] . ';
2075 var gptAddCanonical = ' . (int)$settings['add_canonical'] . ';
2076 var gptAddAlternate = ' . (int)$settings['add_alternate'] . ';
2077 var gptSubfolderInstallation = ' . (int)$settings['subfolder_installation'] . ';
2078 var gptIgnoreQuerystring = ' . (int)$settings['ignore_querystring'] . ';
2079 var gptChatgptGtranslateRequestDelay = ' . (int)$settings['chatgpt_gtranslate_request_delay'] . ';
2080 var gptInitialTranslationDelay = ' . (int)$settings['initial_translation_delay'] . ';
2081 var gptCssSelectorRealtimeTranslationsRetrigger = "' . trim(str_ireplace('"', '', $settings ['css_selector_realtime_translations_retrigger'])) . '";
2082 var chatgptApiKey = "' . esc_js($encoded) . '";
2083 var chatgptApiModel = "' . esc_js($settings['chatgpt_model']) . '";
2084 var chatgptRequestMessage = "' . str_ireplace("\'", "'", esc_js(str_ireplace(['"' , "\r", "\n"], ['' , ' ', ' '], $settings['chatgpt_request_message']))) . '";
2085 var chatgptRequestConversationMode = "' . esc_js($settings['chatgpt_request_conversation_mode']) . '";
2086 var chatgptEnableReader = ' . (int)$settings['enable_reader'] . ';
2087 var chatgptResponsivevoiceLanguageGender = "' . esc_js($settings['responsivevoice_language_gender']) . '";
2088 var chatgptResponsivevoiceApiKey = "' . esc_js($settings['responsivevoice_apikey']) . '";
2089 var chatgptResponsivevoiceReadingMode = "' . esc_js($settings['proxy_responsive_reading_mode']) . '";
2090 var chatgptChunksize = "' . esc_js($settings['chunksize']) . '";
2091 var chatgptCssSelectorLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_leafnodes_excluded'])), ',')) . '";
2092 var chatgptWordsLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['words_leafnodes_excluded'])), ', ')) . '";
2093 var chatgptWordsMinLength = "' . (int)$settings['words_min_length'] . '";
2094 var chatgptFlattenInnerFormattingTags = ' . (int)($settings['flatten_inner_formatting_tags'] ?? 0) . ';
2095 var chatgptFlattenInnerFormattingTagsToRemove = "' . esc_js(str_ireplace('"', '', trim(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", ($settings['flatten_inner_formatting_tags_to_remove'] ?? 'strong,em,u,b,i'))), ',')))) . '";
2096 var chatgptWrapExcludedWords = ' . (int)$settings['wrap_excluded_words'] . ';
2097 var gptApplyDictionaryToAliases = ' . (int)($settings['apply_dictionary_to_aliases'] ?? 0) . ';
2098 var chatgptMainpageSelector = "' . esc_js($settings['mainpage_selector']) . '";
2099 var chatgptElementsToExcludeCustom = "' . esc_js(trim($settings['elements_toexclude_custom'])) . '";
2100 var chatgptPopupFontsize = ' . (int)$settings['popup_fontsize'] . ';
2101 var chatgptDraggableWidget = ' . (int)$settings['draggable_widget'] . ';
2102 var gptAudioVolume = ' . (float)$settings['responsivevoice_volume_tts'] . ';
2103 var gptVoiceSpeed = "' . esc_js($settings['responsivevoice_voice_speed']) . '";
2104 var gTranslateEngine = ' . (($settings['google_translate_engine'] == 1 || !trim($settings['chatgpt_apikey'])) ? 1 : 0) . ';
2105 var gTranslateMethod = ' . (int)($settings['google_translate_method'] ?? 0) . ';
2106 var gptRealtimeTranslationsRetriggerForceGoogle = ' . (int)$settings['realtime_translations_retrigger_force_google'] . ';
2107 var gptRealtimeTranslationsRetriggerDoNotRevert = ' . (int)$settings['realtime_translations_retrigger_do_not_revert'] . ';
2108 var translateEngineValue = "' . esc_js($settings['google_translate_engine']) . '";
2109 var gptPopupShadow = ' . (int)$settings ['popup_shadow'] . ';
2110 var gptDisableControl = ' . (int)$settings['disable_control'] . ';
2111 var gptThemeUri = "' . get_stylesheet_directory_uri() . '";
2112 var gptVersionNumeric = ' . 0 . ';
2113 var svgIconArrow = \'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 285 285"><path d="M282 76.5l-14.2-14.3a9 9 0 0 0-13.1 0L142.5 174.4 30.3 62.2a9 9 0 0 0-13.2 0L3 76.5a9 9 0 0 0 0 13.1l133 133a9 9 0 0 0 13.1 0l133-133a9 9 0 0 0 0-13z" style="fill:%23' . ltrim($settings['widget_text_color'], '#') . '"/></svg>\';';
2114
2115 // Inject it AFTER gptranslate-main-inline
2116 wp_add_inline_script('gptranslate-main-inline', $inlineScript);
2117
2118 wp_register_script('gptranslate-js-specs', '', [], $this->version, true);
2119 wp_enqueue_script('gptranslate-js-specs');
2120 wp_add_inline_script('gptranslate-js-specs', 'window.gptranslateSettings = window.gptranslateSettings || {};
2121 window.gptranslateSettings["1"] = {
2122 "default_language": "' . $settings['language'] . '",
2123 "languages": ' . json_encode($settings['languages']) . ',
2124 "wrapper_selector": "' . $settings['wrapper_selector'] . '",
2125 "float_switcher_open_direction": "' . $settings['float_switcher_open_direction'] . '",
2126 "detect_browser_language": ' . (int)$settings['detect_browser_language'] . ',
2127 "detect_current_language": ' . (int)$settings['detect_current_language'] . ',
2128 "detect_default_language": ' . (int)$settings['detect_default_language'] . ',
2129 "autotranslate_detected_language": ' . (int)$settings['autotranslate_detected_language'] . ',
2130 "always_detect_autotranslated_language": ' . (int)$settings['always_detect_autotranslated_language'] . ',
2131 "widget_text_color": "' . $settings['widget_text_color'] . '",
2132 "show_language_titles": ' . (int)$settings['show_language_titles'] . ',
2133 "enable_dropdown": ' . (int)$settings['enable_dropdown'] . ',
2134 "enable_modal": ' . (int)($settings['enable_modal'] ?? 0) . ',
2135 "equal_widths": ' . (int)$settings['equal_widths'] . ',
2136 "reader_button_position": "' . $settings['reader_button_position'] . '",
2137 "custom_css": "' . addslashes(preg_replace('/\s+/', ' ', str_replace(["\r", "\n"], ' ', (string) $settings['custom_css']))) . '",
2138 "alt_flags": ' . json_encode($alt_flags). ',
2139 "realtime_translations_retrigger_events": ' . json_encode($settings['realtime_translations_retrigger_events']) . ',
2140 "realtime_translations_retrigger_events_delay": ' . (int)$settings['realtime_translations_retrigger_events_delay'] . ',
2141 "switcher_horizontal_position": "' . $switcher_horizontal_position . '",
2142 "switcher_vertical_position": "' . $switcher_vertical_position . '",
2143 "flags_location": "' . esc_js($flagsPath) . '",
2144 "flag_loading": "' . $settings['flag_loading'] . '",
2145 "flag_style": "' . $settings['flag_style'] . '",
2146 "widget_max_height": ' . (int)$settings['widget_max_height'] . ',
2147 "incremental_enabled": ' . (int)($settings['incremental_enabled'] ?? 0) . ',
2148 "incremental_show_progress_popup": ' . (int)($settings['incremental_show_progress_popup'] ?? 0) . ',
2149 "incremental_caseinsensitive": ' . (int)($settings['incremental_caseinsensitive'] ?? 0) . ',
2150 "incremental_filter_excluded_words": ' . (int)($settings['incremental_filter_excluded_words'] ?? 0) . ',
2151 "incremental_filter_dictionary_keys": ' . (int)($settings['incremental_filter_dictionary_keys'] ?? 0) . ',
2152 "incremental_excluded_css_selectors": "' . esc_js($settings['incremental_excluded_css_selectors'] ?? '#wpadminbar,.post-views-count,.post-meta-views,.entry-views,.views-count,.comment-count,.comments-link,.entry-date,.post-date,.publish-date,.updated,.modified,.wp-calendar,.calendar,.widget_recent_posts,.widget_recent_comments,.elementor-counter,.elementor-number-counter,.jet-counter,.counter-box,.rank-math-breadcrumb,.breadcrumb-trail,.wp-pagenumbers,.pagination,.rss-date,.post-meta-date,.social-count,.share-count,.read-more,.more-link,.author-name') . '",
2153 "incremental_excluded_text_patterns": "' . esc_js($settings['incremental_excluded_text_patterns'] ?? '') . '",
2154 "incremental_preserve_new_texts": ' . (int)($settings['incremental_preserve_new_texts'] ?? 1) . '
2155 };');
2156
2157 $languageStringsScript = '';
2158
2159 // Generic translations
2160 $labels = [
2161 'TRANSLATING',
2162 'TRANSLATING_WAIT',
2163 'TRANSLATING_COMPLETE',
2164 'READING_INPROGRESS',
2165 'READING_END',
2166 'READING_EMPTY',
2167 'CHOOSE_LANGUAGE'
2168 ];
2169
2170 foreach ($labels as $label) {
2171 $languageStringsScript .= 'var PLG_GPTRANSLATE_' . $label . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_' . $label)) . '";' . PHP_EOL;
2172 }
2173
2174 $languages = [
2175 'AF', 'SQ', 'AM', 'AR', 'HY', 'AZ', 'EU', 'BE', 'BN', 'BS', 'BG', 'CA', 'CEB', 'NY',
2176 'ZH', 'CO', 'HR', 'CS', 'DA', 'NL', 'EN', 'EO', 'ET', 'TL', 'FI', 'FR',
2177 'FY', 'GL', 'KA', 'DE', 'EL', 'GU', 'HT', 'HA', 'HAW', 'IW', 'HI', 'HMN', 'HU',
2178 'IS', 'IG', 'ID', 'GA', 'IT', 'JA', 'JW', 'KN', 'KK', 'KM', 'KO', 'KU', 'KY', 'LO',
2179 'LA', 'LV', 'LT', 'LB', 'MK', 'MG', 'MS', 'ML', 'MT', 'MI', 'MR', 'MN', 'MY', 'NE',
2180 'NO', 'PS', 'FA', 'PL', 'PT', 'PA', 'RO', 'RU', 'SM', 'GD', 'SR', 'ST', 'SN', 'SD',
2181 'SI', 'SK', 'SL', 'SO', 'ES', 'SU', 'SW', 'SV', 'TG', 'TA', 'TE', 'TH', 'TR', 'UK',
2182 'UR', 'UZ', 'VI', 'CY', 'XH', 'YI', 'YO', 'ZU', 'ZT'
2183 ];
2184
2185
2186 foreach ($languages as $lang) {
2187 $languageStringsScript .= 'var PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang)) . '";' . PHP_EOL;
2188 }
2189
2190 wp_register_script('gptranslate-js-language-strings', '', [], $this->version, true);
2191 wp_enqueue_script('gptranslate-js-language-strings');
2192 wp_add_inline_script('gptranslate-js-language-strings', $languageStringsScript);
2193
2194
2195 // Dictionary
2196 $words_leafnodes_excluded_bylanguage_repeatable = $settings['words_leafnodes_excluded_bylanguage_repeatable'];
2197 if ($words_leafnodes_excluded_bylanguage_repeatable) {
2198 if (is_string($words_leafnodes_excluded_bylanguage_repeatable)) {
2199 $words_leafnodes_excluded_bylanguage_repeatable = json_decode($words_leafnodes_excluded_bylanguage_repeatable, true);
2200 }
2201
2202 // Ora convertiamo l'array normale nel formato con chiavi tipo words_leafnodes_excluded_bylanguage_repeatable0, 1, 2...
2203 $formatted = [];
2204 foreach ($words_leafnodes_excluded_bylanguage_repeatable as $index => $row) {
2205 $formatted["words_leafnodes_excluded_bylanguage_repeatable{$index}"] = [
2206 'words_leafnodes_excluded_bylanguage' => rtrim($row['word'] ?? '', ', '),
2207 'words_leafnodes_excluded_bylanguage_language_original' => $row['langOriginal'] ?? '*',
2208 'words_leafnodes_excluded_bylanguage_language_target' => $row['langTranslated'] ?? '*',
2209 'words_leafnodes_excluded_bylanguage_translation' => rtrim($row['optionalTranslation'] ?? '', ', ')
2210 ];
2211 }
2212
2213 // Correctly formatted JSON encode
2214 $formatted_json = json_encode($formatted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
2215
2216 // Inietto i dati PRIMA che venga eseguito gptranslate.js
2217 wp_register_script('gptranslate-js-word-leafones-excluded-language', '', [], $this->version, true);
2218 wp_enqueue_script('gptranslate-js-word-leafones-excluded-language');
2219 wp_add_inline_script(
2220 'gptranslate-js-word-leafones-excluded-language',
2221 'var chatgptWordsLeafnodesExcludedByLanguage = ' . $formatted_json . ';'
2222 );
2223 }
2224
2225 // Local or remote script
2226 if($settings['proxy_responsive_loading_script'] == 1) {
2227 wp_enqueue_script('gptranslate-responsivevoice', plugin_dir_url(__FILE__) . 'assets/js/responsivevoice.js', [], $this->version, true);
2228 } else {
2229 wp_enqueue_script('gptranslate-responsivevoice', 'https://code.responsivevoice.org/responsivevoice.js?key=' . $settings ['responsivevoice_apikey'], [], $this->version, true);
2230 }
2231
2232 wp_enqueue_script('gptranslate-jsonrepair', plugin_dir_url(__FILE__) . 'assets/js/jsonrepair/index.js', [], $this->version, true);
2233 $gptranslateMainScriptMode = 1;
2234 if (isset($settings['main_script_integrity_check'])) {
2235 $gptranslateMainScriptMode = (int) $settings['main_script_integrity_check'];
2236 }
2237 $gptranslateMainScriptLocalFile = plugin_dir_path(__FILE__) . 'assets/js/gptranslate.js';
2238 $gptranslateMainScriptUrl = plugin_dir_url(__FILE__) . 'assets/js/gptranslate.js';
2239 $gptranslateMainScriptRemoteUrl = 'https://storejextensions.org/cdn/gptranslate/wordpress/gptranslate.js';
2240 $gptranslateMainScriptEmpty = file_exists($gptranslateMainScriptLocalFile) && (int) @filesize($gptranslateMainScriptLocalFile) === 0;
2241 if ($gptranslateMainScriptEmpty && $gptranslateMainScriptMode == 1) {
2242 $response = wp_remote_get($gptranslateMainScriptRemoteUrl, [
2243 'timeout' => 8,
2244 'redirection' => 2,
2245 ]);
2246 if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
2247 $body = wp_remote_retrieve_body($response);
2248 if (is_string($body) && strlen($body) > 1024 && is_writable(dirname($gptranslateMainScriptLocalFile))) {
2249 file_put_contents($gptranslateMainScriptLocalFile, $body, LOCK_EX);
2250 }
2251 }
2252 } elseif ($gptranslateMainScriptEmpty && $gptranslateMainScriptMode == 2) {
2253 $gptranslateMainScriptUrl = $gptranslateMainScriptRemoteUrl;
2254 }
2255 wp_enqueue_script('gptranslate-main', $gptranslateMainScriptUrl, [], $this->version, true);
2256
2257 // Enqueue Bootstrap component
2258 if(!$settings['disable_bootstrap_css']) {
2259 wp_enqueue_script('gptranslate-bstoast', plugin_dir_url(__FILE__) . 'assets/js/toast.min.js', [], $this->version, true);
2260 wp_enqueue_style(
2261 'bootstrap-css',
2262 plugin_dir_url(__FILE__) . 'assets/css/bootstrap.min.css',
2263 [],
2264 '5.3.2'
2265 );
2266 } else {
2267 // Add custom CSS only to replicate the toast and progress styles of Bootstrap
2268 wp_register_style('gptranslate-bootstrap-style', false, [], $this->version);
2269 wp_enqueue_style('gptranslate-bootstrap-style');
2270 wp_add_inline_style('gptranslate-bootstrap-style', '
2271 .progress-gptranslate,.progress-gptranslate-reading{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:0.25rem}
2272 .progress-gptranslate .toast,.progress-gptranslate-reading .toast{width:350px;max-width:100%;font-size:0.875rem;pointer-events:auto;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15);border-radius:0.25rem}
2273 .progress-gptranslate .toast.show,.progress-gptranslate-reading .toast.show{display:block}
2274 .progress-gptranslate .toast-header,.progress-gptranslate-reading .toast-header{display:flex;align-items:center;padding:0.5rem 0.75rem;color:#6c757d;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05);border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}
2275 .progress-gptranslate .toast-body,.progress-gptranslate-reading .toast-body{padding:0.75rem}
2276 .progress-gptranslate .progress-bar,.progress-gptranslate-reading .progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width 0.6s ease}
2277 .progress-gptranslate .progress-bar-striped,.progress-gptranslate-reading .progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}
2278 .progress-gptranslate .progress-bar-animated,.progress-gptranslate-reading .progress-bar-animated{animation:progress-bar-stripes-gptranslate 1s linear infinite}
2279 @keyframes progress-bar-stripes-gptranslate{0%{background-position-x:1rem}}
2280 .progress-gptranslate .btn-close,.progress-gptranslate-reading .btn-close{box-sizing:content-box;width:1em;height:1em;padding:0.25em 0.25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\' fill=\'%23000\'%3e%3cpath d=\'M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z\'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:0.25rem;opacity:0.5;cursor:pointer}
2281 .progress-gptranslate .btn-close:hover,.progress-gptranslate-reading .btn-close:hover{color:#000;text-decoration:none;opacity:0.75}
2282 .progress-gptranslate .me-auto,.progress-gptranslate-reading .me-auto{margin-right:auto!important}
2283 html[dir="rtl"] .progress-gptranslate .me-auto,html[dir="rtl"] .progress-gptranslate-reading .me-auto{margin-right:unset !important;margin-left:auto!important}
2284 .progress-gptranslate .text-muted,.progress-gptranslate-reading .text-muted{color:#6c757d!important}
2285 .progress-gptranslate .bg-primary,.progress-gptranslate-reading .bg-primary{background-color:#0d6efd!important}
2286 .progress-gptranslate .bg-secondary,.progress-gptranslate-reading .bg-secondary{background-color:#6c757d!important}
2287 .progress-gptranslate .bg-success,.progress-gptranslate-reading .bg-success{background-color:#198754!important}
2288 .progress-gptranslate .bg-danger,.progress-gptranslate-reading .bg-danger{background-color:#dc3545!important}
2289 .progress-gptranslate .bg-warning,.progress-gptranslate-reading .bg-warning{background-color:#ffc107!important;color:#000!important}
2290 .progress-gptranslate .bg-info,.progress-gptranslate-reading .bg-info{background-color:#0dcaf0!important;color:#000!important}
2291 .progress-gptranslate .bg-light,.progress-gptranslate-reading .bg-light{background-color:#f8f9fa!important;color:#000!important}
2292 .progress-gptranslate .bg-dark,.progress-gptranslate-reading .bg-dark{background-color:#212529!important}
2293 ');
2294
2295 // Closer for the toast element
2296 wp_register_script('gptranslate-toast-dismiss', false, [], $this->version, true);
2297 wp_enqueue_script('gptranslate-toast-dismiss');
2298 wp_add_inline_script('gptranslate-toast-dismiss', '
2299 document.addEventListener("DOMContentLoaded", function() {
2300 document.addEventListener("click", function(e) {
2301 if (e.target.matches(".btn-close[data-bs-dismiss=\"toast\"]") ||
2302 e.target.closest(".btn-close[data-bs-dismiss=\"toast\"]")) {
2303 const btnClose = e.target.matches(".btn-close") ? e.target : e.target.closest(".btn-close");
2304 const toast = btnClose.closest(".toast");
2305 if (toast) {
2306 toast.classList.remove("show");
2307 const progressContainer = toast.closest(".progress-gptranslate, .progress-gptranslate-reading");
2308 if (progressContainer) {
2309 progressContainer.remove();
2310 }
2311 }
2312 }
2313 });
2314 });
2315 ');
2316 }
2317
2318 // Registra un handle CSS vuoto se necessario
2319 wp_register_style('gptranslate-dynamic-css', false, [], $this->version);
2320 wp_enqueue_style('gptranslate-dynamic-css');
2321
2322 // Prepara lo stile dinamico
2323 $dynamic_css = 'div.gpt_float_switcher .gt-selected, div.gpt_float_switcher, div.gpt_options { background-color: ' . (!empty($settings['widget_background_color']) ? esc_attr($settings['widget_background_color']) : '#FFFFFF') . '; }' .
2324 'div.gpt_float_switcher, div.gpt_float_switcher div.gt-selected div.gpt-current-lang, div.gpt_float_switcher div.gpt_options a { color: ' . (!empty($settings['widget_text_color']) ? esc_attr($settings['widget_text_color']) : '#000000') . '; font-size: ' . intval($settings['popup_fontsize']) . 'px; }' .
2325 'div.gpt_float_switcher { border-radius: ' . intval($settings['popup_border_radius']) . 'px; }' .
2326 'div.gpt_float_switcher img, svg.svg-inline--fa { box-sizing: border-box; width: ' . intval($settings['popup_iconsize']) . 'px; }';
2327
2328 if (!empty($settings['disable_toast_popups']) && $settings['disable_toast_popups'] == 1) {
2329 $dynamic_css .= '.progress.progress-gptranslate,.progress.progress-gptranslate-reading{ display: none !important; }';
2330 }
2331
2332 // Opacity del background widget (solo se diverso da 1.0)
2333 if (!empty($settings['widget_opacity']) && floatval($settings['widget_opacity']) != 1.0) {
2334 $bgColor = !empty($settings['widget_background_color']) ? esc_attr($settings['widget_background_color']) : '#FFFFFF';
2335 $opacity = floatval($settings['widget_opacity']);
2336 $alphaHex = str_pad(dechex(round($opacity * 255)), 2, '0', STR_PAD_LEFT);
2337 $bgColorWithAlpha = $bgColor . strtoupper($alphaHex);
2338
2339 $dynamic_css .= 'div.gpt_float_switcher .gt-selected, div.gpt_float_switcher, div.gpt_options { background-color: ' . $bgColorWithAlpha . ' !important; }';
2340 }
2341
2342 // Inietta il CSS inline
2343 wp_add_inline_style('gptranslate-dynamic-css', $dynamic_css);
2344
2345 // --- Load theme RTL stylesheet if available ---
2346 if ( ! empty( $settings['auto_set_language_direction'] ) && is_rtl() ) {
2347 // Common file names
2348 $rtl_candidates = array(
2349 get_stylesheet_directory() . '/style-rtl.css',
2350 get_stylesheet_directory() . '/rtl.css'
2351 );
2352
2353 $rtl_file = '';
2354 foreach ( $rtl_candidates as $candidate ) {
2355 if ( file_exists( $candidate ) ) {
2356 $rtl_file = $candidate;
2357 break;
2358 }
2359 }
2360
2361 if ( $rtl_file ) {
2362 $rtl_uri = str_replace(
2363 get_stylesheet_directory(),
2364 get_stylesheet_directory_uri(),
2365 $rtl_file
2366 );
2367
2368 wp_enqueue_style(
2369 'theme-rtl',
2370 $rtl_uri,
2371 array(),
2372 filemtime( $rtl_file )
2373 );
2374 }
2375 }
2376 }
2377 }
2378
2379 /**
2380 * Redirect to Wizard only once on fresh install:
2381 * first click on GPTranslate Translations/Settings opens Wizard, then never forced again.
2382 */
2383 add_action('admin_init', function() {
2384 if (!is_admin() || !current_user_can('manage_options')) {
2385 return;
2386 }
2387 if (wp_doing_ajax()) {
2388 return;
2389 }
2390 $page = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2391 if ($page !== 'gptranslate' && $page !== 'gptranslate-settings') {
2392 return;
2393 }
2394 // Fresh install only (option explicitly set to 0 at first install)
2395 if ((int)get_option('gptranslate_wizard_completed', -1) !== 0) {
2396 return;
2397 }
2398 // Force only once
2399 if ((int)get_option('gptranslate_wizard_first_redirect_done', 0) === 1) {
2400 return;
2401 }
2402 update_option('gptranslate_wizard_first_redirect_done', 1, false);
2403 wp_safe_redirect(admin_url('admin.php?page=gptranslate-wizard'));
2404 exit;
2405 });
2406
2407 // Force WordPress.org update check on plugin activation
2408 register_activation_hook( __FILE__, function() {
2409 if ( function_exists('wp_update_plugins') ) {
2410 wp_update_plugins();
2411 }
2412 });
2413
2414 // Schedule a daily update check
2415 add_action( 'gptranslate_daily_update_check', function() {
2416 if ( function_exists('wp_update_plugins') ) {
2417 wp_update_plugins();
2418 }
2419 });
2420
2421 if ( ! wp_next_scheduled( 'gptranslate_daily_update_check' ) ) {
2422 wp_schedule_event( time(), 'daily', 'gptranslate_daily_update_check' );
2423 }
2424
2425 // 🧹 Cleanup scheduled event on deactivation
2426 register_deactivation_hook( __FILE__, function() {
2427 wp_clear_scheduled_hook( 'gptranslate_daily_update_check' );
2428 });
2429
2430 /**
2431 * Add main admin scripts for example to manage records add/delete functions
2432 *
2433 * @access public
2434 */
2435 add_action('admin_enqueue_scripts', function() {
2436 if(isset($_GET['page']) && strpos(sanitize_key($_GET['page']), 'gptranslate') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2437 $currentPage = sanitize_key($_GET['page']);
2438 // Enqueue JS
2439 wp_enqueue_script ( 'gptranslate-js', plugin_dir_url ( __FILE__ ) . 'assets/js/admin.js', [ ], GPTranslate::$pluginVersion, true );
2440 wp_enqueue_script ( 'gptranslate-js-select2', plugin_dir_url ( __FILE__ ) . 'assets/js/select2.min.js', [ 'jquery' ], GPTranslate::$pluginVersion, true );
2441
2442 if ($currentPage === 'gptranslate-wizard') {
2443 wp_enqueue_script('gptranslate-wizard-js', plugin_dir_url(__FILE__) . 'assets/js/wizard.js', [], GPTranslate::$pluginVersion, true);
2444 wp_enqueue_style('gptranslate-wizard-css', plugin_dir_url(__FILE__) . 'assets/css/wizard.css', [], GPTranslate::$pluginVersion);
2445 wp_add_inline_script('gptranslate-wizard-js', '
2446 const PLG_GPTRANSLATE_WIZARD_SAVE_ERROR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_SAVE_ERROR')) . '";
2447 const PLG_GPTRANSLATE_WIZARD_NETWORK_ERROR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_NETWORK_ERROR')) . '";
2448 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP1 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP1')) . '";
2449 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP2 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP2')) . '";
2450 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP3 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP3')) . '";
2451 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP4 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP4')) . '";
2452 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP5 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP5')) . '";
2453 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP6 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP6')) . '";
2454 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP7 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP7')) . '";
2455 const PLG_GPTRANSLATE_WIZARD_DONE_MODEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_DONE_MODEL')) . '";
2456 const PLG_GPTRANSLATE_WIZARD_DONE_TRANSLATION_METHOD = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_DONE_TRANSLATION_METHOD')) . '";
2457 ', 'before');
2458 }
2459
2460 if($currentPage != 'gptranslate-settings' && $currentPage != 'gptranslate-wizard' && !isset($_GET['action'])) {
2461 wp_enqueue_script ( 'crawler-js', plugin_dir_url ( __FILE__ ) . 'assets/js/crawler.js', [ ], GPTranslate::$pluginVersion, true );
2462 }
2463
2464 // Enqueue CSS
2465 wp_enqueue_style ( 'gptranslate-css', plugin_dir_url ( __FILE__ ) . 'assets/css/admin.css', [ ], GPTranslate::$pluginVersion );
2466 wp_enqueue_style ( 'gptranslate-css-select2', plugin_dir_url ( __FILE__ ) . 'assets/css/select2.min.css', [ ], GPTranslate::$pluginVersion );
2467
2468 if($currentPage != 'gptranslate-settings' && $currentPage != 'gptranslate-wizard' && !isset($_GET['action'])) {
2469 wp_enqueue_style ( 'crawler-css', plugin_dir_url ( __FILE__ ) . 'assets/css/crawler.css', [ ], GPTranslate::$pluginVersion );
2470 }
2471
2472 wp_localize_script('gptranslate-js', 'gptranslate_vars', [
2473 'ajaxurl' => admin_url('admin-ajax.php'),
2474 'nonce' => wp_create_nonce('gptranslate_migrate_translations'),
2475 'deletenonce' => wp_create_nonce('gptranslate_delete_translations'),
2476 'gptranslateNonce' => wp_create_nonce('gptranslate_crawler_nonce'),
2477 'testApikeyNonce' => wp_create_nonce('gptranslate_test_apikey'),
2478 'wizardNonce' => wp_create_nonce('gptranslate_wizard_nonce'),
2479 'wizardFirstRun' => (get_option('gptranslate_wizard_completed', null) === 0) ? 1 : 0,
2480 'gptApiKey' => gptranslate_get_api_secret(),
2481 'restNonce' => wp_create_nonce('wp_rest'),
2482 'i18n_test_apikey' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY'),
2483 'i18n_test_apikey_testing' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_TESTING'),
2484 'i18n_test_apikey_success' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_SUCCESS'),
2485 'i18n_test_apikey_error' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_ERROR'),
2486 'i18n_test_apikey_empty' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_EMPTY'),
2487 'i18n_apikey_required' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_APIKEY_REQUIRED')
2488 ]);
2489 } else {
2490 // Not on a GPTranslate page - check if there's an auto-crawl queue for this user
2491 $auto_crawl_queue = get_transient('gpt_auto_crawl_queue_' . get_current_user_id());
2492 if ($auto_crawl_queue && !empty($auto_crawl_queue['url'])) {
2493 // Get settings needed for crawler globals
2494 $settings = get_option('gptranslate_options', []);
2495
2496 // Verify that auto_crawl_on_publish is still enabled
2497 $auto_crawl_enabled = isset($settings['auto_crawl_on_publish']) && ($settings['auto_crawl_on_publish'] == 1 || $settings['auto_crawl_on_publish'] === '1');
2498 if (!$auto_crawl_enabled) {
2499 // Setting has been disabled, clear the queue and exit
2500 delete_transient('gpt_auto_crawl_queue_' . get_current_user_id());
2501 return;
2502 }
2503
2504 // Enqueue crawler.js if not already enqueued
2505 if (!wp_script_is('crawler-js', 'enqueued')) {
2506 // Add inline script with ALL global variables and constants that crawler.js needs
2507 wp_add_inline_script('jquery', '
2508 const PLG_GPTRANSLATE_CRAWLER_TARGET_LINK = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_TARGET_LINK')) . '";
2509 const PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK')) . '";
2510 const PLG_GPTRANSLATE_CRAWLER_START = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_START')) . '";
2511 const PLG_GPTRANSLATE_CRAWLER_START_DESC = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_START_DESC')) . '";
2512 const PLG_GPTRANSLATE_CRAWLER_STARTED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STARTED')) . '";
2513 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING')) . '";
2514 const PLG_GPTRANSLATE_CRAWLER_FOOTER = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_FOOTER')) . '";
2515 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE')) . '";
2516 const PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED')) . '";
2517 const PLG_GPTRANSLATE_CRAWLER_STOP = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOP')) . '";
2518 const PLG_GPTRANSLATE_CRAWLER_STOP_DESC = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOP_DESC')) . '";
2519 const PLG_GPTRANSLATE_CRAWLER_STARTING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STARTING')) . '";
2520 const PLG_GPTRANSLATE_CRAWLER_STOPPING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOPPING')) . '";
2521 const PLG_GPTRANSLATE_CRAWLER_STOPPED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOPPED')) . '";
2522 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
2523 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
2524 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
2525 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
2526 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
2527 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
2528 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
2529 const PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION')) . '";
2530 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL')) . '";
2531 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL')) . '";
2532 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES')) . '";
2533 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED')) . '";
2534 const PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED')) . '";
2535 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION')) . '";
2536 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER')) . '";
2537 var gptranslateBaseCrawlerHome = "' . esc_js(trailingslashit(get_site_url())) . '";
2538 var gptranslateSitemapUrl = "' . esc_js(rest_url('gptranslate/v1/sitemap.xml')) . '";
2539 var gptranslateDefaultLanguage = "' . esc_js($settings['language'] ?? 'en') . '";
2540 var gptranslateCrawlerTimeout = "' . (isset($settings['crawler_timeout']) ? (int)$settings['crawler_timeout'] : '30') . '";
2541 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", ($settings['crawler_exclusions'] ?? ''))), ',')) . '";
2542 var gptranslateCrawlerSkipTranslated = ' . (int)($settings['crawler_skip_translated'] ?? 0) . ';
2543 var gptSubfolderInstallation = ' . (int)($settings['subfolder_installation'] ?? 0) . ';
2544 var gptranslateRewriteLanguageUrl = ' . (int)($settings['rewrite_language_url'] ?? 0) . ';
2545 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
2546 var gptVersionNumeric = 1;
2547 var gptranslateSitemapAvailableLanguages = ' . wp_json_encode(gptranslate_get_available_languages()) . ';
2548 ', 'before');
2549
2550 wp_enqueue_script('crawler-js', plugin_dir_url(__FILE__) . 'assets/js/crawler.js', ['jquery'], GPTranslate::$pluginVersion, true);
2551
2552 // Localize gptranslate_vars for crawler.js
2553 wp_localize_script('crawler-js', 'gptranslate_vars', [
2554 'ajaxurl' => admin_url('admin-ajax.php'),
2555 'gptranslateNonce' => wp_create_nonce('gptranslate_crawler_nonce'),
2556 'gptApiKey' => gptranslate_get_api_secret(),
2557 'restNonce' => wp_create_nonce('wp_rest'),
2558 ]);
2559 }
2560
2561 // Enqueue auto-crawler.js
2562 wp_enqueue_script('auto-crawler-js', plugin_dir_url(__FILE__) . 'assets/js/auto-crawler.js', ['crawler-js'], GPTranslate::$pluginVersion, true);
2563
2564 // Pass the post URL and nonce to auto-crawler.js
2565 wp_localize_script('auto-crawler-js', 'gptAutoCrawlerData', [
2566 'url' => $auto_crawl_queue['url'],
2567 'nonce' => wp_create_nonce('gpt_auto_crawl_nonce')
2568 ]);
2569 }
2570 }
2571 });
2572
2573 // ============================================================================
2574 // AJAX handler for toggling server-side translations during crawler execution
2575 // This prevents conflicts between crawler and server-side translation system
2576 // ============================================================================
2577 add_action('wp_ajax_gptranslate_toggle_serverside', 'gptranslate_toggle_serverside_handler');
2578
2579 /**
2580 * AJAX handler to disable/restore server-side translations during crawler
2581 *
2582 * Actions:
2583 * - 'check': Check current state and disable if enabled
2584 * - 'restore': Restore previous state if it was enabled
2585 *
2586 * This ensures crawler operates without interference from server-side translations
2587 * and automatically restores the original state when crawler stops.
2588 */
2589 function gptranslate_toggle_serverside_handler() {
2590 // Verify nonce for security
2591 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'gptranslate_crawler_nonce')) {
2592 wp_send_json_error(array('message' => 'Invalid security token'));
2593 return;
2594 }
2595
2596 // Verify admin permissions
2597 if (!current_user_can('manage_options')) {
2598 wp_send_json_error(array('message' => 'Unauthorized access'));
2599 return;
2600 }
2601
2602 // Get the action: 'check' or 'restore'
2603 $toggle_action = isset($_POST['toggle_action']) ? sanitize_text_field($_POST['toggle_action']) : '';
2604
2605 if ($toggle_action !== 'check' && $toggle_action !== 'restore') {
2606 wp_send_json_error(array('message' => 'Invalid action parameter'));
2607 return;
2608 }
2609
2610 // Get plugin options from database
2611 // NOTE: Verify this option name matches your actual plugin option name
2612 $option_name = 'gptranslate_options';
2613 $options = get_option($option_name, array());
2614
2615 if (!is_array($options)) {
2616 $options = array();
2617 }
2618
2619 try {
2620 if ($toggle_action === 'check') {
2621 // ACTION CHECK: Check if server-side translations are enabled and disable if necessary
2622
2623 // Get current value
2624 $current_value = isset($options['serverside_translations']) ? $options['serverside_translations'] : '0';
2625
2626 if ($current_value === '1') {
2627 // Server-side translations are enabled, disable them temporarily
2628 $options['serverside_translations'] = '0';
2629
2630 // Update options in database
2631 update_option($option_name, $options);
2632
2633 $message = 'Server-side translations were enabled and have been disabled for crawler';
2634 $action_taken = 'disabled';
2635 } else {
2636 // Already disabled, no action needed
2637 $message = 'Server-side translations were already disabled, no action needed';
2638 $action_taken = 'none';
2639 }
2640
2641 // Return response with original state
2642 wp_send_json_success(array(
2643 'message' => $message,
2644 'were_enabled' => $current_value,
2645 'action_taken' => $action_taken
2646 ));
2647
2648 } else if ($toggle_action === 'restore') {
2649 // ACTION RESTORE: Re-enable only if client tells us they were enabled before
2650
2651 $were_enabled = isset($_POST['were_enabled']) ? sanitize_text_field($_POST['were_enabled']) : '0';
2652
2653 if ($were_enabled === '1') {
2654 // They were enabled before crawler, restore them
2655 $options['serverside_translations'] = '1';
2656
2657 // Update options in database
2658 update_option($option_name, $options);
2659
2660 $message = 'Server-side translations have been restored to enabled';
2661 $action_taken = 'restored';
2662 } else {
2663 // They were not enabled, no action needed
2664 $message = 'Server-side translations were not enabled before, no action needed';
2665 $action_taken = 'none';
2666 }
2667
2668 // Return response
2669 wp_send_json_success(array(
2670 'message' => $message,
2671 'action_taken' => $action_taken
2672 ));
2673 }
2674
2675 } catch (Exception $e) {
2676 wp_send_json_error(array('message' => 'Error: ' . $e->getMessage()));
2677 }
2678 }
2679
2680 // ============================================================================
2681 // AJAX handler for wizard management
2682 // ============================================================================
2683 add_action('wp_ajax_gptranslate_complete_wizard', function () {
2684 check_ajax_referer('gptranslate_wizard_nonce', 'nonce');
2685 if (!current_user_can('manage_options')) {
2686 wp_send_json_error('Unauthorized');
2687 }
2688 update_option('gptranslate_wizard_completed', 1, false);
2689 wp_send_json_success(['completed' => 1]);
2690 });
2691
2692 add_action('wp_ajax_gptranslate_save_wizard', function () {
2693 check_ajax_referer('gptranslate_wizard_nonce', 'nonce');
2694 if (!current_user_can('manage_options')) {
2695 wp_send_json_error('Unauthorized');
2696 }
2697 $opts = get_option('gptranslate_options', []);
2698 $opts['language'] = isset($_POST['language']) ? sanitize_text_field(wp_unslash($_POST['language'])) : ($opts['language'] ?? 'en');
2699 $sourceLang = strtolower((string) $opts['language']);
2700 $rawEnabled = isset($_POST['enabled_languages']) ? wp_unslash($_POST['enabled_languages']) : '[]';
2701 $decodedEnabled = json_decode((string) $rawEnabled, true);
2702 $enabledLangs = [];
2703 if (is_array($decodedEnabled)) {
2704 foreach ($decodedEnabled as $lang) {
2705 $lang = strtolower(sanitize_text_field((string) $lang));
2706 if ($lang !== '') {
2707 $enabledLangs[] = $lang;
2708 }
2709 }
2710 }
2711 $opts['languages'] = array_values(array_unique(array_merge([$sourceLang], $enabledLangs)));
2712 $opts['google_translate_engine'] = isset($_POST['engine']) ? sanitize_text_field(wp_unslash($_POST['engine'])) : ($opts['google_translate_engine'] ?? '1');
2713 $opts['chatgpt_model'] = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : ($opts['chatgpt_model'] ?? 'gpt-5.5');
2714 $opts['google_translate_method'] = isset($_POST['google_method']) ? sanitize_text_field(wp_unslash($_POST['google_method'])) : ($opts['google_translate_method'] ?? '0');
2715 $opts['chatgpt_apikey'] = isset($_POST['apikey']) ? sanitize_text_field(wp_unslash($_POST['apikey'])) : ($opts['chatgpt_apikey'] ?? '');
2716 $opts['serverside_translations'] = isset($_POST['serverside']) ? sanitize_text_field(wp_unslash($_POST['serverside'])) : ($opts['serverside_translations'] ?? '1');
2717 $opts['serverside_translations_method'] = isset($_POST['serverside_method']) ? sanitize_text_field(wp_unslash($_POST['serverside_method'])) : ($opts['serverside_translations_method'] ?? 'regex');
2718 // Keep wizard behavior aligned with main settings: enabling server-side translations
2719 // also enables language detection + autotranslate detected language.
2720 if ((string) $opts['serverside_translations'] === '1') {
2721 $opts['detect_current_language'] = '1';
2722 $opts['autotranslate_detected_language'] = '1';
2723 }
2724 $opts['translate_metadata'] = isset($_POST['metadata']) ? sanitize_text_field(wp_unslash($_POST['metadata'])) : ($opts['translate_metadata'] ?? '0');
2725 $opts['add_canonical'] = isset($_POST['add_canonical']) ? sanitize_text_field(wp_unslash($_POST['add_canonical'])) : ($opts['add_canonical'] ?? '0');
2726 $opts['add_alternate'] = isset($_POST['add_alternate']) ? sanitize_text_field(wp_unslash($_POST['add_alternate'])) : ($opts['add_alternate'] ?? '0');
2727 $opts['set_html_lang'] = isset($_POST['set_html_lang']) ? sanitize_text_field(wp_unslash($_POST['set_html_lang'])) : ($opts['set_html_lang'] ?? '0');
2728 $opts['rewrite_language_url'] = isset($_POST['rewrite_language_url']) ? sanitize_text_field(wp_unslash($_POST['rewrite_language_url'])) : ($opts['rewrite_language_url'] ?? '0');
2729 $opts['rewrite_language_alias'] = isset($_POST['rewrite_language_alias']) ? sanitize_text_field(wp_unslash($_POST['rewrite_language_alias'])) : ($opts['rewrite_language_alias'] ?? '0');
2730 $opts['rewrite_page_links'] = isset($_POST['rewrite_page_links']) ? sanitize_text_field(wp_unslash($_POST['rewrite_page_links'])) : ($opts['rewrite_page_links'] ?? '0');
2731 $opts['float_position'] = isset($_POST['position']) ? sanitize_text_field(wp_unslash($_POST['position'])) : ($opts['float_position'] ?? 'bottom-left');
2732 update_option('gptranslate_options', $opts);
2733 update_option('gptranslate_wizard_completed', 1, false);
2734 wp_send_json_success(['saved' => 1]);
2735 });
2736
2737 // ============================================================================
2738 // AJAX handler for getting already-translated URLs (crawler fast skip)
2739 // ============================================================================
2740 add_action('wp_ajax_gptranslate_get_translated_urls', function () {
2741 check_ajax_referer('gptranslate_crawler_nonce', 'nonce');
2742 if (!current_user_can('manage_options')) {
2743 wp_send_json_error(array('message' => 'Unauthorized'));
2744 return;
2745 }
2746 global $wpdb;
2747 $table = $wpdb->prefix . 'gptranslate';
2748
2749 // Get enabled target languages (excluding the default/source language)
2750 $opts = get_option('gptranslate_options', array());
2751 $defaultLanguage = strtolower($opts['language'] ?? 'en');
2752 $enabledLanguages = array_map('strtolower', $opts['languages'] ?? array());
2753 $targetLanguages = array_values(array_filter($enabledLanguages, function($lang) use ($defaultLanguage) {
2754 return $lang !== $defaultLanguage;
2755 }));
2756 $numTargetLanguages = count($targetLanguages);
2757
2758 if ($numTargetLanguages === 0) {
2759 wp_send_json_success(array('urls' => array()));
2760 return;
2761 }
2762
2763 // Fetch all published rows: pagelink + languagetranslated
2764 // pagelink already contains the language prefix (e.g. http://site/fr/page/)
2765 // so we normalize it in PHP to group by the original URL
2766 $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2767 "SELECT pagelink, languagetranslated FROM {$table} WHERE published = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2768 ARRAY_A
2769 );
2770
2771 // Helper: strip language prefix from URL path
2772 $stripLangPrefix = function($url) {
2773 $cleaned = rtrim($url, '/');
2774 $parsedUrl = parse_url($cleaned);
2775 if ($parsedUrl === false) {
2776 return $cleaned;
2777 }
2778 $pathname = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
2779 if (preg_match('#^/([a-z]{2}(-[a-z]{2})?)(?=/|$)#i', $pathname)) {
2780 $pathname = preg_replace('#^/[a-z]{2}(-[a-z]{2})?#i', '', $pathname);
2781 if (empty($pathname)) {
2782 $pathname = '/';
2783 }
2784 }
2785 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
2786 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
2787 $port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
2788 return $scheme . $host . $port . $pathname;
2789 };
2790
2791 // Group by normalized URL → collect translated languages
2792 $urlLangMap = array();
2793 foreach ($rows as $row) {
2794 $normalizedUrl = $stripLangPrefix($row['pagelink']);
2795 $lang = strtolower($row['languagetranslated']);
2796 if (!isset($urlLangMap[$normalizedUrl])) {
2797 $urlLangMap[$normalizedUrl] = array();
2798 }
2799 if (!in_array($lang, $urlLangMap[$normalizedUrl])) {
2800 $urlLangMap[$normalizedUrl][] = $lang;
2801 }
2802 }
2803
2804 // Keep only URLs where ALL target languages are present
2805 $fullyTranslatedUrls = array();
2806 foreach ($urlLangMap as $normalizedUrl => $translatedLangs) {
2807 $missing = array_diff($targetLanguages, $translatedLangs);
2808 if (empty($missing)) {
2809 $fullyTranslatedUrls[] = $normalizedUrl;
2810 }
2811 }
2812
2813 wp_send_json_success(array('urls' => $fullyTranslatedUrls));
2814 });
2815
2816 // ============================================================================
2817 // AJAX handler for testing API key validity
2818 // ============================================================================
2819 add_action('wp_ajax_gptranslate_test_apikey', function () {
2820 // Verify nonce
2821 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'gptranslate_test_apikey')) {
2822 wp_send_json_error(array('message' => 'Invalid security token'));
2823 return;
2824 }
2825
2826 // Verify admin permissions
2827 if (!current_user_can('manage_options')) {
2828 wp_send_json_error(array('message' => 'Unauthorized access'));
2829 return;
2830 }
2831
2832 $apiKey = isset($_POST['apikey']) ? sanitize_text_field(wp_unslash($_POST['apikey'])) : '';
2833 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
2834
2835 if (empty($apiKey)) {
2836 wp_send_json_error(array('message' => 'API key is empty'));
2837 return;
2838 }
2839
2840 $url = '';
2841 $headers = array('Content-Type' => 'application/json');
2842 $body = '';
2843
2844 try {
2845 if (strpos($model, 'gpt-') === 0 || strpos($model, 'grok-') === 0) {
2846 // OpenAI / ChatGPT / Grok
2847 $url = (strpos($model, 'grok-') === 0) ? 'https://api.x.ai/v1/chat/completions' : 'https://api.openai.com/v1/chat/completions';
2848 $headers['Authorization'] = 'Bearer ' . $apiKey;
2849 $useNewTokenParam = (strpos($model, 'gpt-4.1') === 0 || strpos($model, 'gpt-5') === 0);
2850 $tokenParam = $useNewTokenParam ? 'max_completion_tokens' : 'max_tokens';
2851 $body = wp_json_encode(array(
2852 'model' => $model,
2853 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2854 $tokenParam => 5
2855 ));
2856 } elseif (strpos($model, 'deepseek-') === 0) {
2857 // DeepSeek
2858 $url = 'https://api.deepseek.com/v1/chat/completions';
2859 $headers['Authorization'] = 'Bearer ' . $apiKey;
2860 $body = wp_json_encode(array(
2861 'model' => $model,
2862 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2863 'max_tokens' => 5
2864 ));
2865 } elseif (strpos($model, 'gemini-') === 0) {
2866 // Google Gemini
2867 $apiVersion = (strpos($model, '-preview') !== false) ? 'v1beta' : 'v1';
2868 $url = "https://generativelanguage.googleapis.com/{$apiVersion}/models/{$model}:generateContent";
2869 $headers['x-goog-api-key'] = $apiKey;
2870 $body = wp_json_encode(array(
2871 'contents' => array(array('parts' => array(array('text' => 'Hi')))),
2872 'generationConfig' => array('maxOutputTokens' => 5)
2873 ));
2874 } elseif (strpos($model, 'claude-') === 0) {
2875 // Claude / Anthropic
2876 $url = 'https://api.anthropic.com/v1/messages';
2877 $headers['x-api-key'] = $apiKey;
2878 $headers['anthropic-version'] = '2023-06-01';
2879 unset($headers['Authorization']);
2880 $body = wp_json_encode(array(
2881 'model' => $model,
2882 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2883 'max_tokens' => 5
2884 ));
2885 } elseif ($model === 'google-cloud-translation-api') {
2886 // Google Cloud Translation
2887 $url = 'https://translation.googleapis.com/language/translate/v2?key=' . urlencode($apiKey);
2888 $body = wp_json_encode(array(
2889 'q' => array('hello'),
2890 'source' => 'en',
2891 'target' => 'es',
2892 'format' => 'text'
2893 ));
2894 } elseif ($model === 'deepl-api') {
2895 // DeepL - Auto-detect endpoint based on API key type
2896 // Free API keys end with ':fx', paid keys don't have this suffix
2897 $deeplEndpoint = (strpos ( $apiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
2898 $url = $deeplEndpoint . '/v2/translate';
2899 $headers ['Authorization'] = 'DeepL-Auth-Key ' . $apiKey;
2900 $headers ['Content-Type'] = 'application/x-www-form-urlencoded';
2901 // DeepL requires repeated "text" params (not text[0])
2902 $body = 'text=' . urlencode ( 'hello' ) . '&source_lang=EN&target_lang=ES';
2903 } else {
2904 wp_send_json_error(array('message' => 'Unsupported model: ' . $model));
2905 return;
2906 }
2907
2908 $response = wp_remote_post($url, array(
2909 'headers' => $headers,
2910 'body' => $body,
2911 'timeout' => 60
2912 ));
2913
2914 if (is_wp_error($response)) {
2915 wp_send_json_error(array(
2916 'message' => $response->get_error_message(),
2917 'error_code' => 0,
2918 'http_code' => 0
2919 ));
2920 return;
2921 }
2922
2923 $httpCode = wp_remote_retrieve_response_code($response);
2924 $responseBody = wp_remote_retrieve_body($response);
2925
2926 if ($httpCode >= 200 && $httpCode < 300) {
2927 wp_send_json_success(array(
2928 'result' => true,
2929 'http_code' => $httpCode
2930 ));
2931 } else {
2932 $errorMessage = 'HTTP ' . $httpCode;
2933 $decoded = json_decode($responseBody, true);
2934 if ($decoded) {
2935 if (isset($decoded['error']['message'])) {
2936 $errorMessage = $decoded['error']['message'];
2937 } elseif (isset($decoded['error']['status'])) {
2938 $errorMessage = $decoded['error']['status'];
2939 }
2940 // Claude/Anthropic error format: {type: "error", error: {type, message}}
2941 if (isset($decoded['type']) && $decoded['type'] === 'error' && isset($decoded['error']['message'])) {
2942 $errorMessage = $decoded['error']['message'];
2943 }
2944 }
2945 wp_send_json_error(array(
2946 'message' => $errorMessage,
2947 'error_code' => $httpCode,
2948 'http_code' => $httpCode
2949 ));
2950 }
2951 } catch (Exception $e) {
2952 wp_send_json_error(array(
2953 'message' => $e->getMessage(),
2954 'error_code' => 0,
2955 'http_code' => 0
2956 ));
2957 }
2958 });
2959
2960 add_action('wp_ajax_gptranslate_bulk_delete', function () {
2961 if (!current_user_can('manage_options') || !check_ajax_referer('gptranslate_delete_translations', '_wpnonce', false)) {
2962 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2963 }
2964
2965 global $wpdb;
2966 $ids = isset($_POST['gptid']) ? array_map('intval', (array) $_POST['gptid']) : [];
2967
2968 if (empty($ids)) {
2969 wp_send_json_error('No records selected');
2970 }
2971
2972 $table = $wpdb->prefix . 'gptranslate';
2973 $in = implode(',', array_fill(0, count($ids), '%d'));
2974 $sql = "DELETE FROM {$table} WHERE id IN ($in)";
2975 $result = $wpdb->query($wpdb->prepare($sql, ...$ids)); // phpcs:ignore
2976
2977 if ($result === false) {
2978 wp_send_json_error('Database error');
2979 }
2980
2981 wp_send_json_success();
2982 });
2983
2984 // Handle Export CSV
2985 add_action('admin_post_gptranslate_export_translations_csv', function () {
2986 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_csv', 'gptranslate_export_csv_nonce')) {
2987 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
2988 }
2989
2990 global $wpdb;
2991 $table = $wpdb->prefix . 'gptranslate';
2992
2993 $records = $wpdb->get_results("SELECT * FROM $table ORDER BY translate_date DESC", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2994
2995 if (!$records) {
2996 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
2997 }
2998
2999 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3000 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
3001
3002 header('Content-Type: text/csv');
3003 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.csv"');
3004
3005 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
3006 $output = fopen('php://output', 'w');
3007
3008 // Intestazioni CSV
3009 fputcsv($output, array_keys($records[0]), ",", '"', "\\");
3010
3011 foreach ($records as $record) {
3012 fputcsv($output, $record, ",", '"', "\\");
3013 }
3014
3015 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
3016 fclose($output);
3017 exit;
3018 });
3019
3020 // Handle Import CSV
3021 add_action('admin_post_gptranslate_import_translations_csv', function () {
3022 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_csv', 'gptranslate_import_csv_nonce')) {
3023 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3024 }
3025
3026 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
3027 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
3028 }
3029
3030 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3031 if (!is_uploaded_file($tmp_name)) {
3032 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3033 }
3034
3035 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
3036 $file = fopen($tmp_name, 'r');
3037 if (!$file) {
3038 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3039 }
3040
3041 $headers = fgetcsv($file);
3042 if (!$headers) {
3043 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_CSV_FORMAT')));
3044 }
3045
3046 global $wpdb;
3047 $table = $wpdb->prefix . 'gptranslate';
3048
3049 while (($row = fgetcsv($file)) !== false) {
3050 $countHeaders = count($headers);
3051 $countRow = count($row);
3052 // Invalid combine
3053 if($countHeaders != $countRow) {
3054 continue;
3055 }
3056 $record = array_combine($headers, $row);
3057 if (empty($record['pagelink'])) {
3058 continue; // skip if no primary key
3059 }
3060
3061 $pagelink = sanitize_text_field($record['pagelink']);
3062 $exists = $wpdb->get_var($wpdb->prepare(
3063 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
3064 $pagelink, $record['languageoriginal'], $record['languagetranslated']
3065 ));// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3066
3067 $data = [
3068 'translated_alias' => $record['translated_alias'],
3069 'translations' => $record['translations'],
3070 'alt_translations' => $record['alt_translations'],
3071 'languageoriginal' => sanitize_text_field($record['languageoriginal']),
3072 'languagetranslated' => sanitize_text_field($record['languagetranslated']),
3073 'published' => isset($record['published']) ? (int)$record['published'] : 1,
3074 'translate_date' => $record['translate_date'],
3075 'translation_engine' => sanitize_text_field($record['translation_engine']),
3076 ];
3077
3078 if ($exists) {
3079 $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3080 $table,
3081 $data,
3082 ['pagelink' => $pagelink, 'languageoriginal' => sanitize_text_field($record['languageoriginal']), 'languagetranslated' => sanitize_text_field($record['languagetranslated'])],
3083 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s'],
3084 ['%s','%s','%s']
3085 );
3086 } else {
3087 $data['pagelink'] = $pagelink;
3088 $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3089 $table,
3090 $data,
3091 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
3092 );
3093 }
3094 }
3095
3096 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
3097 fclose($file);
3098
3099 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
3100 exit;
3101 });
3102
3103 // Handle Export XLIFF
3104 add_action('admin_post_gptranslate_export_translations_xliff', function () {
3105 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce')) {
3106 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3107 }
3108
3109 global $wpdb;
3110 $table = $wpdb->prefix . 'gptranslate';
3111
3112 $records = $wpdb->get_results("SELECT * FROM $table ORDER BY translate_date DESC", ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3113
3114 if (!$records) {
3115 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
3116 }
3117
3118 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3119 $fileDate = (date_i18n('Y-m-d', strtotime($localDate)));
3120
3121 header('Content-Type: application/xml; charset=utf-8');
3122 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.xliff"');
3123
3124 $xml = new SimpleXMLElement('<xliff/>');
3125 $xml->addAttribute('version', '1.2');
3126
3127 foreach ($records as $record) {
3128 $file = $xml->addChild('file');
3129 $file->addAttribute('source-language', $record['languageoriginal']);
3130 $file->addAttribute('target-language', $record['languagetranslated']);
3131 $file->addAttribute('datatype', 'html');
3132 $file->addAttribute('original', $record['pagelink']);
3133
3134 $body = $file->addChild('body');
3135
3136 $translations = json_decode($record['translations'], true) ?: [];
3137 foreach ($translations as $source => $target) {
3138 $unit = $body->addChild('trans-unit');
3139 $unit->addAttribute('id', md5($source));
3140 $unit->addChild('source', htmlspecialchars($source));
3141 $unit->addChild('target', htmlspecialchars($target));
3142 }
3143 }
3144
3145 echo $xml->asXML();
3146 exit;
3147 });
3148
3149 // Handle Import XLIFF
3150 add_action('admin_post_gptranslate_import_translations_xliff', function () {
3151 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce')) {
3152 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3153 }
3154
3155 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
3156 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
3157 }
3158
3159 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3160 if (!is_uploaded_file($tmp_name)) {
3161 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3162 }
3163
3164 $xml = simplexml_load_file($tmp_name);
3165 if (!$xml) {
3166 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_XLIFF_FORMAT')));
3167 }
3168
3169 global $wpdb;
3170 $table = $wpdb->prefix . 'gptranslate';
3171
3172 foreach ($xml->file as $file) {
3173 $sourceLang = (string)$file['source-language'];
3174 $targetLang = (string)$file['target-language'];
3175 $pagelink = (string)$file['original'];
3176
3177 $translations = [];
3178 foreach ($file->body->{'trans-unit'} as $unit) {
3179 $src = (string)$unit->source;
3180 $tgt = (string)$unit->target;
3181 $translations[$src] = $tgt;
3182 }
3183
3184 $json_translations = wp_json_encode($translations);
3185
3186 $exists = $wpdb->get_var($wpdb->prepare(
3187 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
3188 $pagelink, $sourceLang, $targetLang
3189 )); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
3190
3191 if ($exists) {
3192 // �
3193 Update only the translations and date, keep everything else intact
3194 $wpdb->update(
3195 $table,
3196 [
3197 'translations' => $json_translations,
3198 'translate_date' => current_time('mysql'),
3199 ],
3200 ['id' => $exists]
3201 ); // phpcs:ignore
3202 } else {
3203 // �
3204 Insert full record only if it doesn't exist
3205 $data = [
3206 'pagelink' => $pagelink,
3207 'translated_alias' => '',
3208 'translations' => $json_translations,
3209 'alt_translations' => '[]',
3210 'languageoriginal' => $sourceLang,
3211 'languagetranslated'=> $targetLang,
3212 'published' => 1,
3213 'translate_date' => current_time('mysql'),
3214 'translation_engine'=> 'chatgpt',
3215 ];
3216 $wpdb->insert($table, $data); // phpcs:ignore
3217 }
3218 }
3219
3220 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
3221 exit;
3222 });
3223
3224 // Handle Export XML Sitemap
3225 add_action('admin_post_gptranslate_export_xml_sitemap', function () {
3226 if (!current_user_can('manage_options') || !isset($_POST['gptranslate_export_xml_sitemap']) || $_POST['gptranslate_export_xml_sitemap'] !== 'b62d18a19b') {
3227 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3228 }
3229
3230 global $wpdb;
3231 $table = $wpdb->prefix . 'gptranslate';
3232
3233 // Get language filter from POST parameter
3234 $language_filter = isset($_POST['sitemap_language']) ? sanitize_text_field(wp_unslash($_POST['sitemap_language'])) : '';
3235
3236 // Solo record con translated_alias non vuoto
3237 $query = "
3238 SELECT *
3239 FROM $table
3240 WHERE translated_alias IS NOT NULL
3241 AND translated_alias != ''";
3242
3243 // Add language filter if specified and not "all"
3244 if (!empty($language_filter) && $language_filter !== 'all') {
3245 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
3246 }
3247
3248 $query .= " ORDER BY translate_date DESC";
3249
3250 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3251
3252 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3253 $fileDate = date_i18n('Y-m-d', strtotime($localDate));
3254
3255 // Add language suffix to filename if specific language is selected
3256 if (!empty($language_filter) && $language_filter !== 'all') {
3257 $fileDate .= '-' . sanitize_file_name($language_filter);
3258 }
3259
3260 $dom = new DOMDocument('1.0', 'UTF-8');
3261 $dom->preserveWhiteSpace = false;
3262 $dom->formatOutput = true;
3263
3264 $urlset = $dom->createElement('urlset');
3265 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
3266 $dom->appendChild($urlset);
3267
3268 if (empty($records)) {
3269 $urlset->appendChild($dom->createTextNode(''));
3270 }
3271
3272 foreach ($records as $record) {
3273 $url = $dom->createElement('url');
3274 $loc = $dom->createElement('loc');
3275 $loc->appendChild($dom->createTextNode($record['translated_alias']));
3276 $url->appendChild($loc);
3277
3278 if (!empty($record['translate_date'])) {
3279 $lastmod = $dom->createElement('lastmod', date('c', strtotime($record['translate_date'])));
3280 $url->appendChild($lastmod);
3281 }
3282
3283 $urlset->appendChild($url);
3284 }
3285
3286 header('Content-Type: application/xml; charset=UTF-8');
3287 header('Content-Disposition: attachment; filename="gptranslate-sitemap-' . $fileDate . '.xml"');
3288 echo $dom->saveXML();
3289 exit;
3290 });
3291
3292 // Register Ajax handler
3293 add_action('wp_ajax_gptranslate_migrate_translations', function() {
3294 if (!current_user_can('manage_options')) {
3295 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3296 }
3297
3298 check_ajax_referer('gptranslate_migrate_translations');
3299
3300 global $wpdb;
3301 $table = $wpdb->prefix . 'gptranslate';
3302 $old = isset($_POST['old_domain']) ? sanitize_text_field(wp_unslash($_POST['old_domain'])) : '';
3303 $new = isset($_POST['new_domain']) ? sanitize_text_field(wp_unslash($_POST['new_domain'])) : '';
3304
3305 if (empty($old) || empty($new)) {
3306 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_MISSING_DOMAIN_VALUES')));
3307 }
3308
3309 $query = $wpdb->prepare(
3310 "UPDATE $table SET pagelink = REPLACE(pagelink, %s, %s), translated_alias = REPLACE(translated_alias, %s, %s)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3311 $old, $new, $old, $new
3312 );
3313
3314 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
3315 $result = $wpdb->query($query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3316
3317 if ($result === false) {
3318 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_DATABASE_ERROR')));
3319 } else {
3320 wp_send_json_success();
3321 }
3322 });
3323
3324 function gptranslate_export_settings() {
3325 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_export_settings', 'gptranslate_export_settings_nonce')) {
3326 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
3327 }
3328
3329 $options = get_option( 'gptranslate_options', [] );
3330
3331 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3332 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
3333
3334 header( 'Content-Type: application/json' );
3335 header( 'Content-Disposition: attachment; filename="gptranslate-settings-' . $fileDate . '.json"' );
3336 echo wp_json_encode( $options );
3337 exit;
3338 }
3339 add_action( 'admin_post_gptranslate_export_settings', 'gptranslate_export_settings' );
3340
3341 function gptranslate_import_settings() {
3342 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_import_settings', 'gptranslate_import_settings_nonce')) {
3343 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
3344 }
3345
3346 if ( empty( $_FILES['gptranslate_settings_file']['tmp_name'] ) ) {
3347 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')) );
3348 }
3349
3350 $content = file_get_contents( $_FILES['gptranslate_settings_file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3351 $decoded = json_decode( $content, true );
3352
3353 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
3354 wp_die( esc_html(GPTranslate::loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS') ) );
3355 }
3356
3357 // Optional: sanitize known values, or just update if you're confident of source
3358 update_option( 'gptranslate_options', $decoded );
3359
3360 wp_safe_redirect( admin_url( 'admin.php?page=gptranslate-settings&settingsimported=1' ) );
3361 exit;
3362 }
3363 add_action( 'admin_post_gptranslate_import_settings', 'gptranslate_import_settings' );
3364
3365 // ============================================================================
3366 // Shortcode [gptranslate] - Renders the language switcher at the shortcode position
3367 // ============================================================================
3368 add_shortcode('gptranslate', function ($atts) {
3369 $settings = get_option("gptranslate_options");
3370
3371 // Disable interface
3372 if (!empty($settings['disable_control'])) {
3373 return '';
3374 }
3375
3376 $wrapper_class = 'gptranslate_wrapper';
3377 $custom_selector = $settings['wrapper_selector'] ?? '.gptranslate_wrapper';
3378
3379 // If the user has set a custom wrapper selector (not the default), use that class
3380 if ($custom_selector !== '.gptranslate_wrapper' && strpos($custom_selector, '.') === 0) {
3381 $wrapper_class = substr($custom_selector, 1);
3382 }
3383
3384 // Flag that the shortcode was used, to prevent duplicate wrapper in footer
3385 if (!defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
3386 define('GPTRANSLATE_SHORTCODE_RENDERED', true);
3387 }
3388
3389 return '<div class="' . esc_attr($wrapper_class) . '"></div>';
3390 });
3391
3392 add_action('wp_footer', function () {
3393 // Add the default target container if default CSS selector
3394 $settings = get_option("gptranslate_options");
3395
3396 // Disable interface
3397 if($settings ['disable_control']) {
3398 $settings ['wrapper_selector'] = '';
3399 }
3400
3401 // Skip the automatic footer wrapper if shortcode was already used
3402 if (defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
3403 return;
3404 }
3405
3406 if ($settings ['wrapper_selector'] == '.gptranslate_wrapper') {
3407 echo '<div class="gptranslate_wrapper" id="gpt-wrapper"></div>';
3408 }
3409 });
3410
3411 // Get available (enabled) languages for sitemap filtering (excluding original language)
3412 function gptranslate_get_available_languages() {
3413 $opts = get_option('gptranslate_options', array());
3414
3415 // Get enabled target languages from settings
3416 $enabled_languages = isset($opts['languages']) ? array_map('strtolower', (array)$opts['languages']) : array();
3417
3418 // Exclude the original/source language
3419 $original_language = isset($opts['language']) ? strtolower($opts['language']) : '';
3420 $enabled_languages = array_filter($enabled_languages, function($lang) use ($original_language) {
3421 return $lang !== $original_language;
3422 });
3423
3424 return $enabled_languages ? array_values($enabled_languages) : array();
3425 }
3426
3427 // ============================================================================
3428 // AUTO-CRAWL ON POST PUBLISH
3429 // Automatically crawl new/updated posts when they are published
3430 // ============================================================================
3431
3432 /**
3433 * Hook triggered when a post is published/saved.
3434 * Saves the post URL to a transient queue if auto-crawl is enabled.
3435 */
3436 add_action('wp_after_insert_post', function($post_id, $post, $update, $post_before) {
3437 // Check if auto-crawl is enabled in settings
3438 $settings = get_option('gptranslate_options', []);
3439 // Handle both string '1' and integer 1 for the setting value
3440 $auto_crawl_enabled = isset($settings['auto_crawl_on_publish']) && ($settings['auto_crawl_on_publish'] == 1 || $settings['auto_crawl_on_publish'] === '1');
3441
3442 if (!$auto_crawl_enabled) {
3443 return;
3444 }
3445
3446 // Only trigger auto-crawl when post transitions TO published status (first publish only)
3447 $was_published = $post_before && $post_before->post_status === 'publish';
3448 $is_now_published = $post->post_status === 'publish';
3449
3450 // Must be newly published (not already published before)
3451 if (!($is_now_published && !$was_published)) {
3452 return;
3453 }
3454
3455 // Skip post types that shouldn't be translated (attachments, revisions, etc.)
3456 $exclude_types = ['attachment', 'revision', 'nav_menu_item'];
3457 if (in_array($post->post_type, $exclude_types, true)) {
3458 return;
3459 }
3460
3461 // Skip if this is an auto-draft
3462 if ($post->post_type === 'post' || $post->post_type === 'page') {
3463 // Get the post URL
3464 $post_url = get_permalink($post_id);
3465 if (empty($post_url)) {
3466 return;
3467 }
3468
3469 // Add language prefix to URL if needed (prevents 301 redirect that strips UTF-8 chars)
3470 $settings = get_option('gptranslate_options', []);
3471 if (!empty($settings['rewrite_language_url']) && empty($settings['omit_prefix_original_language'])) {
3472 $default_lang = $settings['language'] ?? 'en';
3473
3474 if (!empty($settings['subfolder_installation'])) {
3475 // Subfolder installation: add language prefix after subfolder
3476 // e.g., http://site.com/subfolder/page → http://site.com/subfolder/en/page
3477 $site_url = trailingslashit(get_site_url());
3478 $relative_path = str_replace($site_url, '', $post_url);
3479 $post_url = $site_url . $default_lang . '/' . ltrim($relative_path, '/');
3480 } else {
3481 // Root installation: add language prefix after domain
3482 // e.g., http://site.com/page → http://site.com/en/page
3483 $post_url = preg_replace(
3484 '#^(https?://[^/]+)/#',
3485 '$1/' . $default_lang . '/',
3486 $post_url
3487 );
3488 }
3489 }
3490
3491 // Save the URL to transient queue (per-user, 5 minute TTL)
3492 $queue_key = 'gpt_auto_crawl_queue_' . get_current_user_id();
3493 set_transient($queue_key, [
3494 'url' => $post_url,
3495 'post_id' => $post_id,
3496 'timestamp' => time()
3497 ], 5 * MINUTE_IN_SECONDS);
3498 }
3499 }, 10, 4);
3500
3501 /**
3502 * AJAX handler to clear the auto-crawl queue.
3503 * Called by auto-crawler.js after the post is published and user navigates to admin.
3504 * This prevents re-triggering the crawl on page reload.
3505 */
3506 add_action('wp_ajax_gpt_clear_auto_crawl_queue', function() {
3507 // Verify nonce for security
3508 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'gpt_auto_crawl_nonce')) {
3509 wp_send_json_error(['message' => 'Invalid nonce']);
3510 wp_die();
3511 }
3512
3513 // Delete the transient queue for current user
3514 $queue_key = 'gpt_auto_crawl_queue_' . get_current_user_id();
3515 delete_transient($queue_key);
3516
3517 wp_send_json_success();
3518 wp_die();
3519 });
3520
3521 // POST API REST Translations storage
3522 add_action('rest_api_init', function () {
3523 register_rest_route('gptranslate/v1', '/request', [
3524 'methods' => 'POST',
3525 'callback' => 'gpt_handle_request',
3526 'permission_callback' => 'gptranslate_public_permission'
3527 ]);
3528 });
3529
3530 // Real-time XML Sitemap endpoint - public, generates sitemap on-the-fly
3531 add_action('rest_api_init', function () {
3532 register_rest_route('gptranslate/v1', '/sitemap.xml', [
3533 'methods' => 'GET',
3534 'callback' => 'gptranslate_realtime_sitemap',
3535 'permission_callback' => '__return_true'
3536 ]);
3537 });
3538
3539 function gptranslate_realtime_sitemap() {
3540 global $wpdb;
3541 $table = $wpdb->prefix . 'gptranslate';
3542
3543 // Get language filter from query parameter
3544 $language_filter = isset($_GET['language']) ? sanitize_text_field(wp_unslash($_GET['language'])) : '';
3545
3546 $query = "
3547 SELECT translated_alias, translate_date
3548 FROM $table
3549 WHERE translated_alias IS NOT NULL
3550 AND translated_alias != ''";
3551
3552 // Add language filter if specified and not "all"
3553 if (!empty($language_filter) && $language_filter !== 'all') {
3554 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
3555 }
3556
3557 $query .= " ORDER BY translate_date DESC";
3558
3559 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3560
3561 $dom = new DOMDocument('1.0', 'UTF-8');
3562 $dom->preserveWhiteSpace = false;
3563 $dom->formatOutput = true;
3564
3565 $urlset = $dom->createElement('urlset');
3566 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
3567 $dom->appendChild($urlset);
3568
3569 if (empty($records)) {
3570 $urlset->appendChild($dom->createTextNode(''));
3571 }
3572
3573 foreach ($records as $record) {
3574 $url = $dom->createElement('url');
3575 $loc = $dom->createElement('loc');
3576 $loc->appendChild($dom->createTextNode($record['translated_alias']));
3577 $url->appendChild($loc);
3578
3579 if (!empty($record['translate_date'])) {
3580 $lastmod = $dom->createElement('lastmod', gmdate('c', strtotime($record['translate_date'])));
3581 $url->appendChild($lastmod);
3582 }
3583
3584 $urlset->appendChild($url);
3585 }
3586
3587 header('Content-Type: application/xml; charset=UTF-8');
3588 echo $dom->saveXML();
3589 exit;
3590 }
3591
3592 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
3593 // Remove the default 'Settings' item
3594 unset($links[0]);
3595
3596 $wizard_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-wizard')) . '">Wizard</a>';
3597 array_unshift($links, $wizard_link);
3598
3599 $settings_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-settings')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
3600 array_unshift($links, $settings_link);
3601
3602 $translations_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_TRANSLATIONS')) . '</a>';
3603 array_unshift($links, $translations_link);
3604
3605 return $links;
3606 });
3607
3608 /*
3609 // Remove any WP update for the free version over the paid full one
3610 add_filter('auto_update_plugin', function($update, $item) {
3611 if (isset($item->slug) && $item->slug === 'gptranslate') {
3612 return false;
3613 }
3614 return $update;
3615 }, 10, 2);
3616
3617
3618 add_filter('site_transient_update_plugins', function($transient) {
3619 if (isset($transient->response['gptranslate/gptranslate.php'])) {
3620 unset($transient->response['gptranslate/gptranslate.php']);
3621 }
3622 return $transient;
3623 });
3624 */
3625
3626 /**
3627 * Get (or lazily create) the cryptographically random API secret.
3628 *
3629 * Replaces the predictable hash('sha256', get_site_url()) shared secret.
3630 * The value is generated once with wp_generate_password() (which uses
3631 * a cryptographically secure RNG) and persisted in wp_options.
3632 *
3633 * @return string
3634 */
3635 function gptranslate_get_api_secret() {
3636 $secret = get_option( 'gptranslate_api_secret' );
3637 if ( empty( $secret ) || ! is_string( $secret ) || strlen( $secret ) < 32 ) {
3638 $secret = wp_generate_password( 64, false, false );
3639 update_option( 'gptranslate_api_secret', $secret, false );
3640 }
3641 return $secret;
3642 }
3643
3644 /**
3645 * Permission callback public API
3646 * @param WP_REST_Request $request
3647 * @return bool|WP_Error
3648 */
3649 function gptranslate_public_permission( WP_REST_Request $request ) {
3650 // 1) Random API secret sent via header (constant-time comparison)
3651 $headerApiKey = (string) $request->get_header('x-gptranslate-key');
3652 $restApiKey = gptranslate_get_api_secret();
3653 if ( ! hash_equals( $restApiKey, $headerApiKey ) ) {
3654 return new WP_Error( 'rest_forbidden', esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FORBIDDEN_APIKEY')), [ 'status' => 403 ] );
3655 }
3656
3657 // 2) Nonce verification for write operations (CSRF protection)
3658 $params = $request->get_json_params();
3659 if ( empty( $params ) ) {
3660 $params = $request->get_body_params();
3661 }
3662 $task = isset( $params['task'] ) ? sanitize_text_field( $params['task'] ) : '';
3663 $writeTasks = array( 'storetranslations', 'syncTranslation' );
3664 if ( in_array( $task, $writeTasks, true ) ) {
3665 $nonce = $request->get_header('X-WP-Nonce');
3666 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
3667 return new WP_Error( 'rest_forbidden_nonce', 'Invalid or expired security token (nonce).', [ 'status' => 403 ] );
3668 }
3669 }
3670
3671 return true;
3672 }
3673
3674 /**
3675 * Normalize WP URL
3676 * @param string $url
3677 * @return string
3678 */
3679 function gpt_trailingslashit_url($url) {
3680 $parsed = wp_parse_url($url);
3681 if (empty($parsed['path'])) {
3682 $parsed['path'] = '/';
3683 } else {
3684 $parsed['path'] = trailingslashit(untrailingslashit($parsed['path']));
3685 }
3686
3687 $rebuilt = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : '';
3688 $rebuilt .= $parsed['host'] ?? '';
3689 $rebuilt .= $parsed['path'];
3690 if (!empty($parsed['query'])) {
3691 $rebuilt .= '?' . $parsed['query'];
3692 }
3693 if (!empty($parsed['fragment'])) {
3694 $rebuilt .= '#' . $parsed['fragment'];
3695 }
3696
3697 return $rebuilt;
3698 }
3699
3700 /**
3701 * Callback per GET/STORE translations via REST.
3702 * Frontend API
3703 *
3704 * @param WP_REST_Request $request
3705 * @return WP_REST_Response
3706 */
3707 function gpt_handle_request( WP_REST_Request $request ) {
3708 global $wpdb;
3709
3710 $table = $wpdb->prefix . 'gptranslate';
3711
3712 $params = $request->get_json_params();
3713
3714 if(!$params) {
3715 $params = $request->get_body_params();
3716 }
3717
3718 // Sanitize input params
3719 $task = sanitize_text_field( $params['task'] ?? '' );
3720 $pageLink = esc_url_raw( $params['pagelink'] ?? '' );
3721 $translatedAlias = esc_url_raw( $params['translated_alias'] ?? '' );
3722 $languageOriginal = sanitize_text_field( $params['language_original'] ?? '' );
3723 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
3724 $translationEngine = sanitize_text_field( $params['translation_engine'] ?? '' );
3725 $retriggerTranslation = (int) sanitize_text_field( $params['retrigger'] ?? false );
3726 $isIncremental = (int) sanitize_text_field( $params['incremental'] ?? 0 );
3727
3728 $now = current_time( 'mysql' );
3729
3730 $response = [ 'result' => false ];
3731
3732 if ( $task === 'storetranslations' ) {
3733 // Ensure there is not a mismatching insert with the same languages
3734 if($languageOriginal == $languageTranslated) {
3735 $response['result'] = true;
3736 return rest_ensure_response( $response );
3737 }
3738
3739 // Fetch raw param (could be array or JSON string)
3740 $rawFull = $params['translations'] ?? '[]';
3741 $rawAlt = $params['alt_translations'] ?? '[]';
3742
3743 // If it’s already a string (JSON), use it directly.
3744 // If it’s an array (unlikely with FormData), JSON encode it.
3745 if ( is_string( $rawFull ) && json_decode( $rawFull ) !== null ) {
3746 $fullTranslations = $rawFull;
3747 } else {
3748 $fullTranslations = wp_json_encode( (array) $rawFull );
3749 }
3750
3751 if ( is_string( $rawAlt ) && json_decode( $rawAlt ) !== null ) {
3752 $altTranslations = $rawAlt;
3753 } else {
3754 $altTranslations = wp_json_encode( (array) $rawAlt );
3755 }
3756
3757 // Check if record already exists
3758 $existing = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3759 "SELECT id FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3760 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3761 "\n AND languageoriginal = %s" .
3762 "\n AND languagetranslated = %s",
3763 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3764 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
3765
3766 $existing_id = $existing ? (int)$existing : null;
3767 $opts = get_option('gptranslate_options', []);
3768
3769 // Only if it is a retrigger then ignore the db processing
3770 if($retriggerTranslation !== 1) {
3771 if ( $existing_id ) {
3772 if ( $isIncremental ) {
3773 // Incremental update: ALWAYS merge only new/modified translations (safe - never touch alt_translations or translated_alias)
3774 $existing_record = $wpdb->get_row( $wpdb->prepare(
3775 "SELECT translations FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
3776 " WHERE id = %d",
3777 (int) $existing_id
3778 ), ARRAY_A );
3779
3780 if ( $existing_record ) {
3781 // Merge translations: respect lock_translations setting
3782 $existing_trans = json_decode( $existing_record['translations'], true ) ?: [];
3783 $new_trans = json_decode( $fullTranslations, true ) ?: [];
3784
3785 // If lock_translations enabled: preserve existing (even more conservative on incremental)
3786 // If lock_translations disabled: allow updates to existing translations
3787 if ( !empty($opts['lock_translations']) ) {
3788 $merged_trans = array_merge( $new_trans, $existing_trans ); // existing wins
3789 } else {
3790 $merged_trans = array_merge( $existing_trans, $new_trans ); // new wins (updates)
3791 }
3792
3793 $update_data = [
3794 'translations' => wp_json_encode( $merged_trans ),
3795 'translate_date' => $now,
3796 'translation_engine' => $translationEngine,
3797 // Never touch alt_translations or translated_alias on incremental updates
3798 ];
3799
3800 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3801 $table,
3802 $update_data,
3803 [ 'id' => (int) $existing_id ],
3804 [ '%s', '%s', '%s' ],
3805 [ '%d' ]
3806 );
3807 $response['result'] = ( $updated !== false );
3808 } else {
3809 $response['result'] = false;
3810 }
3811 } else {
3812 // Full (non-incremental) update: protected by lock_translations setting
3813 if ( !empty($opts['lock_translations']) ) {
3814 // lock_translations enabled: skip full UPDATE
3815 $response['result'] = false;
3816 } else {
3817 // lock_translations disabled: overwrite everything (2.29 behavior)
3818 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3819 $table,
3820 [
3821 'translations' => $fullTranslations,
3822 'alt_translations' => $altTranslations,
3823 'translated_alias' => $translatedAlias,
3824 'translate_date' => $now,
3825 'translation_engine' => $translationEngine,
3826 ],
3827 [ 'id' => (int) $existing_id ],
3828 [ '%s', '%s', '%s', '%s', '%s' ],
3829 [ '%d' ]
3830 );
3831 $response['result'] = ( $updated !== false );
3832 }
3833 }
3834 } else {
3835 // INSERT
3836 $insert_data = [
3837 'pagelink' => $pageLink,
3838 'translations' => $fullTranslations,
3839 'alt_translations' => $altTranslations,
3840 'translated_alias' => $translatedAlias,
3841 'languageoriginal' => $languageOriginal,
3842 'languagetranslated' => $languageTranslated,
3843 'published' => 1,
3844 'translate_date' => $now,
3845 'translation_engine' => $translationEngine,
3846 ];
3847
3848 $inserted = $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3849 $table,
3850 $insert_data,
3851 [ '%s','%s','%s','%s','%s','%s','%d','%s','%s' ]
3852 );
3853 $response['result'] = ( $inserted !== false );
3854 }
3855 } else {
3856 $response['result'] = true;
3857 }
3858 } elseif ($task === 'gettranslations') {
3859 $opts = get_option('gptranslate_options', []);
3860 if (!empty($opts['realtime_translations']) || $retriggerTranslation === 1) {
3861 $response['result'] = false;
3862 } else {
3863 // Prepare decoded version for URL matching
3864 $pageLinkDecoded = urldecode($pageLink);
3865
3866 if ($opts['rewrite_language_url'] == 1 && $opts['rewrite_language_alias'] == 1) {
3867 // Check 8 variants (with/without slash + encoded/decoded)
3868 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3869 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3870 "\n WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s OR translated_alias = %s OR translated_alias = %s OR translated_alias = %s OR translated_alias = %s)" .
3871 "\n AND languageoriginal = %s" .
3872 "\n AND languagetranslated = %s" .
3873 "\n AND published = 1",
3874 rtrim($pageLink, '/'),
3875 rtrim($pageLink, '/') . '/',
3876 rtrim($pageLinkDecoded, '/'),
3877 rtrim($pageLinkDecoded, '/') . '/',
3878 rtrim($pageLink, '/'),
3879 rtrim($pageLink, '/') . '/',
3880 rtrim($pageLinkDecoded, '/'),
3881 rtrim($pageLinkDecoded, '/') . '/',
3882 $languageOriginal,
3883 $languageTranslated
3884 ), ARRAY_A);
3885 } else {
3886 // Check 4 variants (with/without slash + encoded/decoded)
3887 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3888 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3889 "\n WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s)" .
3890 "\n AND languageoriginal = %s" .
3891 "\n AND languagetranslated = %s" .
3892 "\n AND published = 1",
3893 rtrim($pageLink, '/'),
3894 rtrim($pageLink, '/') . '/',
3895 rtrim($pageLinkDecoded, '/'),
3896 rtrim($pageLinkDecoded, '/') . '/',
3897 $languageOriginal,
3898 $languageTranslated
3899 ), ARRAY_A);
3900 }
3901
3902 if ($row) {
3903 $response['result'] = true;
3904 $response['translations'] = json_decode($row['translations'], true) ?: [];
3905 $response['alt_translations'] = json_decode($row['alt_translations'], true) ?: [];
3906 $response['translated_alias'] = $row['translated_alias'];
3907 $response['pagelink_alias'] = $row['pagelink'];
3908 } else {
3909 $response['result'] = false;
3910 }
3911 }
3912 } elseif ($task == 'getaliastranslation') {
3913 // Always perform a new realtime translation if the option is enabled
3914 try {
3915 $row = $wpdb->get_row( $wpdb->prepare(
3916 "SELECT translated_alias FROM {$table}" .
3917 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3918 "\n AND languageoriginal = %s" .
3919 "\n AND languagetranslated = %s" .
3920 "\n AND published = 1",
3921 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3922 ), ARRAY_A );
3923
3924 if ( $row ) {
3925 $response['result'] = true;
3926 $response['translated_alias'] = $row['translated_alias'] ?? '';
3927 } else {
3928 $response['result'] = false;
3929 }
3930 } catch ( Exception $e ) {
3931 $response['result'] = false;
3932 $response['exception'] = $e->getMessage();
3933 }
3934 } elseif ( $task === 'syncTranslation' ) {
3935 $original = wp_unslash( $params['original'] ?? '' );
3936 $translated = wp_unslash( $params['translated'] ?? '' );
3937 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
3938 $translationType = sanitize_text_field( $params['translation_type'] ?? 'translations' ); // default to 'translations'
3939
3940 // Recupera tutti i record nella lingua target
3941 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3942 "SELECT id, {$translationType}, languagetranslated" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3943 "\n FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3944 "\n WHERE languagetranslated = %s",
3945 $languageTranslated
3946 ) );
3947
3948 $updatedCount = 0;
3949
3950 foreach ( $rows as $row ) {
3951 $currentTranslations = json_decode( $row->$translationType, true );
3952
3953 if ( is_array( $currentTranslations ) && array_key_exists( $original, $currentTranslations ) ) {
3954 // Aggiorna la traduzione e salva
3955 $currentTranslations[ $original ] = $translated;
3956 $jsonUpdated = wp_json_encode( $currentTranslations );
3957
3958 $success = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3959 $table,
3960 [ $translationType => $jsonUpdated, 'translate_date' => $now ],
3961 [ 'id' => $row->id ],
3962 [ '%s', '%s' ],
3963 [ '%d' ]
3964 );
3965
3966 if ( $success !== false ) {
3967 $updatedCount++;
3968 }
3969 }
3970 }
3971
3972 $response['result'] = $updatedCount > 0;
3973 } elseif ($task == 'gettranslatedaliases') {
3974 try {
3975 if ($languageTranslated) {
3976 $rows = $wpdb->get_results(
3977 $wpdb->prepare(
3978 "SELECT pagelink, translated_alias
3979 FROM {$table}
3980 WHERE languagetranslated = %s
3981 AND published = 1",
3982 $languageTranslated
3983 ),
3984 ARRAY_A
3985 );
3986 } elseif ($languageOriginal) {
3987 $rows = $wpdb->get_results(
3988 $wpdb->prepare(
3989 "SELECT translated_alias AS pagelink, pagelink AS translated_alias
3990 FROM {$table}
3991 WHERE languageoriginal = %s
3992 AND published = 1",
3993 $languageOriginal
3994 ),
3995 ARRAY_A
3996 );
3997 } else {
3998 $response['result'] = false;
3999 echo wp_json_encode($response);
4000 exit;
4001 }
4002
4003 if ($rows) {
4004 $encodedResult = [];
4005
4006 foreach ($rows as $row) {
4007
4008 // Normalizza WordPress-style (CON trailing slash)
4009 $pagelink = gpt_trailingslashit_url($row['pagelink']);
4010 $translatedAlias = !empty($row['translated_alias']) ? gpt_trailingslashit_url($row['translated_alias']) : '';
4011
4012 // Encode pagelink (solo path)
4013 $parsedUrl = wp_parse_url($pagelink);
4014 $encodedPagelink = $pagelink;
4015
4016 if (!empty($parsedUrl['path'])) {
4017 $pathParts = explode('/', $parsedUrl['path']);
4018 $encodedParts = array_map('rawurlencode', $pathParts);
4019 $encodedPath = implode('/', $encodedParts);
4020
4021 $encodedPagelink = ($parsedUrl['scheme'] ?? '') . '://' . ($parsedUrl['host'] ?? '');
4022 $encodedPagelink .= $encodedPath;
4023 if (!empty($parsedUrl['query'])) {
4024 $encodedPagelink .= '?' . $parsedUrl['query'];
4025 }
4026 if (!empty($parsedUrl['fragment'])) {
4027 $encodedPagelink .= '#' . $parsedUrl['fragment'];
4028 }
4029 }
4030
4031 // Encode translated alias
4032 $encodedAlias = $translatedAlias;
4033 if (!empty($translatedAlias)) {
4034 $parsedAlias = wp_parse_url($translatedAlias);
4035 if (!empty($parsedAlias['path'])) {
4036 $pathParts = explode('/', $parsedAlias['path']);
4037 $encodedParts = array_map('rawurlencode', $pathParts);
4038 $encodedPath = implode('/', $encodedParts);
4039
4040 $encodedAlias = ($parsedAlias['scheme'] ?? '') . '://' . ($parsedAlias['host'] ?? '');
4041 $encodedAlias .= $encodedPath;
4042 if (!empty($parsedAlias['query'])) {
4043 $encodedAlias .= '?' . $parsedAlias['query'];
4044 }
4045 if (!empty($parsedAlias['fragment'])) {
4046 $encodedAlias .= '#' . $parsedAlias['fragment'];
4047 }
4048 }
4049 }
4050
4051 $encodedResult[$encodedPagelink] = [
4052 'pagelink' => $encodedPagelink,
4053 'translated_alias' => $encodedAlias
4054 ];
4055 }
4056
4057 $response['result'] = true;
4058 $response['translated_aliases'] = $encodedResult;
4059
4060 } else {
4061 $response['result'] = false;
4062 }
4063
4064 } catch (Exception $e) {
4065 $response['result'] = false;
4066 $response['exception'] = $e->getMessage();
4067 }
4068 } elseif ( $task === 'deepseektranslations' ) {
4069 try {
4070 // Read raw JSON payload sent from JS
4071 $rawInput = file_get_contents( 'php://input' );
4072 $requestData = json_decode( $rawInput, true );
4073
4074 if ( ! $requestData || empty( $requestData['messages'] ) ) {
4075 throw new Exception( 'Invalid DeepSeek request payload' );
4076 }
4077
4078 // Get plugin options
4079 $opts = get_option( 'gptranslate_options', [] );
4080
4081 $deepseekApiKey = $opts['chatgpt_apikey'] ?? '';
4082 $deepseekModel = $opts['chatgpt_model'] ?? 'deepseek-chat';
4083
4084 if ( empty( $deepseekApiKey ) ) {
4085 throw new Exception( 'DeepSeek API key not configured' );
4086 }
4087
4088 // Fixed DeepSeek parameters (server controlled)
4089 $payload = [
4090 'model' => $deepseekModel,
4091 'messages' => $requestData['messages'],
4092 'max_tokens' => 4096,
4093 'temperature' => 0.5,
4094 ];
4095
4096 // Call DeepSeek API
4097 $ch = curl_init( 'https://api.deepseek.com/v1/chat/completions' );
4098 curl_setopt_array( $ch, [
4099 CURLOPT_POST => true,
4100 CURLOPT_RETURNTRANSFER => true,
4101 CURLOPT_HTTPHEADER => [
4102 'Content-Type: application/json',
4103 'Authorization: Bearer ' . $deepseekApiKey,
4104 ],
4105 CURLOPT_POSTFIELDS => wp_json_encode( $payload ),
4106 CURLOPT_TIMEOUT => 60,
4107 ] );
4108
4109 $apiResponse = curl_exec( $ch );
4110
4111 if ( $apiResponse === false ) {
4112 throw new Exception( 'DeepSeek cURL error: ' . curl_error( $ch ) );
4113 }
4114
4115 $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
4116 curl_close( $ch );
4117
4118 if ( $httpCode >= 400 ) {
4119 throw new Exception( 'DeepSeek API HTTP error: ' . $httpCode );
4120 }
4121
4122 // IMPORTANT:
4123 // Return DeepSeek response EXACTLY as the API returns it
4124 header( 'Content-Type: application/json; charset=utf-8' );
4125 echo $apiResponse;
4126 exit;
4127
4128 } catch ( Exception $e ) {
4129 // FALLBACK SAFE RESPONSE (OpenAI-compatible)
4130 header('Content-Type: application/json; charset=utf-8');
4131 echo wp_json_encode([
4132 'choices' => [[
4133 'finish_reason' => 'error',
4134 'message' => [
4135 'role' => 'assistant',
4136 'content' => ''
4137 ]
4138 ]]
4139 ]);
4140 exit;
4141 }
4142 } elseif ($task === 'deepltranslations') {
4143 try {
4144 // Use $params already parsed by WordPress REST API (php://input is consumed)
4145 if (empty ( $params ['texts'] )) {
4146 throw new Exception ( 'Invalid DeepL request payload' );
4147 }
4148
4149 // Get plugin options
4150 $opts = get_option ( 'gptranslate_options', [ ] );
4151
4152 $deeplApiKey = $opts ['chatgpt_apikey'] ?? '';
4153
4154 if (empty ( $deeplApiKey )) {
4155 throw new Exception ( 'DeepL API key not configured' );
4156 }
4157
4158 $sourceLanguage = sanitize_text_field ( $params ['source_lang'] ?? 'auto');
4159 $targetLanguage = sanitize_text_field ( $params ['target_lang'] ?? 'EN');
4160
4161 // Build query string manually - DeepL requires repeated "text" params (not text[0], text[1])
4162 $queryParts = [ ];
4163 $queryParts [] = 'source_lang=' . urlencode ( strtoupper ( $sourceLanguage ) );
4164 $queryParts [] = 'target_lang=' . urlencode ( strtoupper ( $targetLanguage ) );
4165
4166 foreach ( $params ['texts'] as $text ) {
4167 $queryParts [] = 'text=' . urlencode ( $text );
4168 }
4169
4170 $queryString = implode ( '&', $queryParts );
4171
4172 // Call DeepL API - Auto-detect endpoint based on API key type
4173 // Free API keys end with ':fx', paid keys don't have this suffix
4174 $deeplEndpoint = (strpos ( $deeplApiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
4175 $ch = curl_init ( $deeplEndpoint . '/v2/translate' );
4176 curl_setopt_array ( $ch, [
4177 CURLOPT_POST => true,
4178 CURLOPT_RETURNTRANSFER => true,
4179 CURLOPT_HTTPHEADER => [
4180 'Authorization: DeepL-Auth-Key ' . $deeplApiKey,
4181 'Content-Type: application/x-www-form-urlencoded'
4182 ],
4183 CURLOPT_POSTFIELDS => $queryString,
4184 CURLOPT_TIMEOUT => 60
4185 ] );
4186
4187 $apiResponse = curl_exec ( $ch );
4188
4189 if ($apiResponse === false) {
4190 throw new Exception ( 'DeepL cURL error: ' . curl_error ( $ch ) );
4191 }
4192
4193 $httpCode = curl_getinfo ( $ch, CURLINFO_HTTP_CODE );
4194 curl_close ( $ch );
4195
4196 if ($httpCode >= 400) {
4197 throw new Exception ( 'DeepL API HTTP error: ' . $httpCode . ' - ' . $apiResponse );
4198 }
4199
4200 // Return DeepL response EXACTLY as the API returns it
4201 header ( 'Content-Type: application/json; charset=utf-8' );
4202 echo $apiResponse;
4203 exit ();
4204 } catch ( Exception $e ) {
4205 wp_send_json_error ( [
4206 'message' => $e->getMessage ()
4207 ] );
4208 }
4209 }
4210
4211 return rest_ensure_response( $response );
4212 }
4213
4214 // Instantiate and run the app
4215 GPTranslate::get_instance();
4216
4217 // Optimizer compatibility – Phase 1 (early).
4218 // PHP-side exclusion filters must be registered at plugin load time so they
4219 // are in place before any optimizer (e.g. WP Rocket) reads them during its
4220 // own initialisation. Never runs in wp-admin (handled inside init_early).
4221 GPTranslate_Optimizer_Compat::init_early();
4222
4223 // Optimizer compatibility – Phase 2 (late).
4224 // HTML tag-manipulation filters (script_loader_tag, wp_inline_script_tag)
4225 // are registered here, after GPTranslate has decided whether to enqueue
4226 // scripts on this page. Bails immediately if gptranslate-main is not queued.
4227 add_action( 'wp_enqueue_scripts', [ 'GPTranslate_Optimizer_Compat', 'init_late' ], 999 );