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