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 9 hours ago flags 4 days ago includes 4 days ago language 4 days ago ajax-handler.php 4 days ago gptranslate.php 11 hours ago multilang-routing.php 4 days ago readme.txt 4 days ago serverside-translations.php 4 days ago settings.php 4 days ago simplehtmldom.php 4 days ago uninstall.php 4 days ago
gptranslate.php
4301 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 // DeepSeek calls are proxied server-side (see 'deepseektranslations' AJAX task below),
2076 // so the real key never needs to reach the browser for this engine.
2077 $isDeepseekEngine = isset($settings['chatgpt_model']) && strpos($settings['chatgpt_model'], 'deepseek-') === 0;
2078 $key = ($isDeepseekEngine || !isset($settings['chatgpt_apikey'])) ? '' : (string) $settings['chatgpt_apikey'];
2079 $keyParts = $key !== '' ? array_reverse(str_split($key, 4)) : [];
2080 $encodedParts = [];
2081 foreach ($keyParts as $index => $part) {
2082 $encodedParts[] = strrev($part);
2083 $encodedParts[] = 'gptx' . (($index + 17) * 13);
2084 }
2085 $encoded = wp_json_encode($encodedParts, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
2086 if ($encoded === false) {
2087 $encoded = '[]';
2088 }
2089
2090 $ajaxEndpoint = esc_url_raw(rest_url('gptranslate/v1/request'));
2091
2092 $inlineScript = 'var gptServerSideLink = "' . esc_js($ajaxEndpoint) . '";';
2093 if (!empty($settings['lightweight_ajax_endpoint'])) {
2094 $lightweightEndpoint = esc_url_raw(plugin_dir_url(__FILE__) . 'ajax-handler.php');
2095 $inlineScript .= '
2096 var gptServerSideLightLink = "' . esc_js($lightweightEndpoint) . '";';
2097 }
2098 $inlineScript .= '
2099 var gptApiKey = "' . esc_js(gptranslate_get_api_secret()) . '";
2100 var gptAjaxSecret = "' . esc_js(gptranslate_get_api_secret()) . '";
2101 var gptRestNonce = "' . esc_js(wp_create_nonce('wp_rest')) . '";
2102 var gptLiveSite = "' . esc_js(get_site_url()) . '";
2103 var gptStorage = ' . ($settings['storage_type'] === 'session' ? 'window.sessionStorage' : 'window.localStorage') . ';
2104 var gptMaxTranslationsPerRequest = ' . (int)$settings['max_translations_per_request'] . ';
2105 var maxCharactersPerRequest = ' . (int)$settings['max_characters_per_request'] . ';
2106 var gptRewriteLanguageUrl = ' . (int)$settings['rewrite_language_url'] . ';
2107 var gptOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
2108 var gptExcludedAliasSlugs = "' . esc_js(rtrim($settings['excluded_alias_slugs'], ', ')) . '";
2109 var gptRewriteLanguageAlias = ' . (int)$settings['rewrite_language_alias'] . ';
2110 var gptRewriteLanguageAliasOriginalLanguage = ' . (int)$settings['rewrite_language_alias_original_language'] . ';
2111 var gptAutoSetLanguageDirection = ' . (int)$settings['auto_set_language_direction'] . ';
2112 var gptServersideTranslations = ' . (int)$settings['serverside_translations'] . ';
2113 var gptServersideTranslationsLanguageSwitchingMode = "' . $settings ['serverside_translations_language_switching_mode'] . '";
2114 var gptServersideTranslationsMatchquotes = ' . (int)$settings['serverside_translations_matchquotes'] . ';
2115 var gptRewritePageLinks = ' . (int)$settings['rewrite_page_links'] . ';
2116 var gptRewritePageLinksExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($settings['rewrite_page_links_exclusions']) ? $settings['rewrite_page_links_exclusions'] : ''))), ',')) . '";
2117 var gptPageExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", ($settings['page_exclusions'] ?? ''))), ',')) . '";
2118 var gptRewriteFormActions = ' . (int)$settings['rewrite_form_actions'] . ';
2119 var gptTransliterateUrls = ' . (int)$settings ['transliterate_urls'] . ';
2120 var gptForceAsciiUrlAliases = ' . (int)$settings ['force_ascii_url_aliases'] . ';
2121 var gptTranslateMetadata = ' . (int)$settings['translate_metadata'] . ';
2122 var gptTranslatePlaceholders = ' . (int)$settings['translate_placeholders'] . ';
2123 var gptTranslateAltImages = ' . (int)$settings['translate_altimages'] . ';
2124 var chatgptClassesAltimagesExcluded = "' . esc_js(str_ireplace('"', '', $settings['css_selector_classes_translate_altimages_excluded'])) . '";
2125 var gptTranslateSrcImages = ' . (int)$settings['translate_srcimages'] . ';
2126 var gptTranslateSrcIframes = ' . (int)$settings['translate_srciframes'] . ';
2127 var gptTranslateSrcVideos = ' . (int)$settings['translate_srcvideos'] . ';
2128 var gptTranslateIframeLocale = ' . (int)$settings['translate_iframe_locale'] . ';
2129 var gptTranslateTitles = ' . (int)$settings['translate_titles'] . ';
2130 var gptTranslateValues = ' . (int)$settings['translate_values'] . ';
2131 var gptMetadataChosenEngine = ' . (isset($settings['metadata_chosen_engine']) ? (int)$settings['metadata_chosen_engine'] : 0) . ';
2132 var chatgptMetadataWordsLeafnodesExcluded = "' . esc_js(rtrim($settings['metadata_words_leafnodes_excluded'], ', ')) . '";
2133 var gptSetHtmlLang = ' . (int)$settings['set_html_lang'] . ';
2134 var gptAddCanonical = ' . (int)$settings['add_canonical'] . ';
2135 var gptAddAlternate = ' . (int)$settings['add_alternate'] . ';
2136 var gptSubfolderInstallation = ' . (int)$settings['subfolder_installation'] . ';
2137 var gptIgnoreQuerystring = ' . (int)$settings['ignore_querystring'] . ';
2138 var gptChatgptGtranslateRequestDelay = ' . (int)$settings['chatgpt_gtranslate_request_delay'] . ';
2139 var gptInitialTranslationDelay = ' . (int)$settings['initial_translation_delay'] . ';
2140 var gptCssSelectorRealtimeTranslationsRetrigger = "' . trim(str_ireplace('"', '', $settings ['css_selector_realtime_translations_retrigger'])) . '";
2141 var chatgptApiKey = ' . $encoded . ';
2142 var chatgptApiModel = "' . esc_js($settings['chatgpt_model']) . '";
2143 var chatgptRequestMessage = "' . str_ireplace("\'", "'", esc_js(str_ireplace(['"' , "\r", "\n"], ['' , ' ', ' '], $settings['chatgpt_request_message']))) . '";
2144 var chatgptRequestConversationMode = "' . esc_js($settings['chatgpt_request_conversation_mode']) . '";
2145 var chatgptEnableReader = ' . (int)$settings['enable_reader'] . ';
2146 var chatgptResponsivevoiceLanguageGender = "' . esc_js($settings['responsivevoice_language_gender']) . '";
2147 var chatgptResponsivevoiceApiKey = "' . esc_js($settings['responsivevoice_apikey']) . '";
2148 var chatgptResponsivevoiceReadingMode = "' . esc_js($settings['proxy_responsive_reading_mode']) . '";
2149 var chatgptChunksize = "' . esc_js($settings['chunksize']) . '";
2150 var chatgptCssSelectorLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_leafnodes_excluded'])), ',')) . '";
2151 var chatgptWordsLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['words_leafnodes_excluded'])), ', ')) . '";
2152 var chatgptWordsMinLength = "' . (int)$settings['words_min_length'] . '";
2153 var chatgptFlattenInnerFormattingTags = ' . (int)($settings['flatten_inner_formatting_tags'] ?? 0) . ';
2154 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'))), ',')))) . '";
2155 var chatgptWrapExcludedWords = ' . (int)$settings['wrap_excluded_words'] . ';
2156 var gptApplyDictionaryToAliases = ' . (int)($settings['apply_dictionary_to_aliases'] ?? 0) . ';
2157 var chatgptMainpageSelector = "' . esc_js($settings['mainpage_selector']) . '";
2158 var chatgptElementsToExcludeCustom = "' . esc_js(trim($settings['elements_toexclude_custom'])) . '";
2159 var chatgptPopupFontsize = ' . (int)$settings['popup_fontsize'] . ';
2160 var chatgptDraggableWidget = ' . (int)$settings['draggable_widget'] . ';
2161 var gptAudioVolume = ' . (float)$settings['responsivevoice_volume_tts'] . ';
2162 var gptVoiceSpeed = "' . esc_js($settings['responsivevoice_voice_speed']) . '";
2163 var gTranslateEngine = ' . (($settings['google_translate_engine'] == 1 || !trim($settings['chatgpt_apikey'])) ? 1 : 0) . ';
2164 var gTranslateMethod = ' . (int)($settings['google_translate_method'] ?? 0) . ';
2165 var gptRealtimeTranslationsRetriggerForceGoogle = ' . (int)$settings['realtime_translations_retrigger_force_google'] . ';
2166 var gptRealtimeTranslationsRetriggerDoNotRevert = ' . (int)$settings['realtime_translations_retrigger_do_not_revert'] . ';
2167 var gptRealtimeTranslationsRetriggerScopeSelector = "' . esc_js($settings['realtime_translations_retrigger_scope_selector'] ?? '') . '";
2168 var translateEngineValue = "' . esc_js($settings['google_translate_engine']) . '";
2169 var gptPopupShadow = ' . (int)$settings ['popup_shadow'] . ';
2170 var gptDisableControl = ' . (int)$settings['disable_control'] . ';
2171 var gptThemeUri = "' . get_stylesheet_directory_uri() . '";
2172 var gptVersionNumeric = ' . 0 . ';
2173 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>\';';
2174
2175 $customUrlPrefixSwaps = [];
2176 foreach (explode(',', $settings['custom_url_prefixes'] ?? '') as $mapping) {
2177 $parts = explode(':', trim($mapping));
2178 if (count($parts) === 2) {
2179 $from = strtolower(trim($parts[0]));
2180 $to = strtolower(trim($parts[1]));
2181 if ($from && $to) $customUrlPrefixSwaps[$from] = $to;
2182 }
2183 }
2184 if (!empty($customUrlPrefixSwaps)) {
2185 $settings['languages'] = array_map(
2186 function($lang) use ($customUrlPrefixSwaps) {
2187 return $customUrlPrefixSwaps[$lang] ?? $lang;
2188 },
2189 $settings['languages'] ?? []
2190 );
2191 }
2192 $inlineScript .= 'var gptCustomUrlPrefixSwaps = ' . wp_json_encode($customUrlPrefixSwaps) . ';';
2193
2194 // Inject it AFTER gptranslate-main-inline
2195 wp_add_inline_script('gptranslate-main-inline', $inlineScript);
2196
2197 wp_register_script('gptranslate-js-specs', '', [], $this->version, true);
2198 wp_enqueue_script('gptranslate-js-specs');
2199 wp_add_inline_script('gptranslate-js-specs', 'window.gptranslateSettings = window.gptranslateSettings || {};
2200 window.gptranslateSettings["1"] = {
2201 "default_language": "' . $settings['language'] . '",
2202 "languages": ' . json_encode($settings['languages']) . ',
2203 "wrapper_selector": "' . $settings['wrapper_selector'] . '",
2204 "float_switcher_open_direction": "' . $settings['float_switcher_open_direction'] . '",
2205 "detect_browser_language": ' . (int)$settings['detect_browser_language'] . ',
2206 "detect_current_language": ' . (int)$settings['detect_current_language'] . ',
2207 "detect_default_language": ' . (int)$settings['detect_default_language'] . ',
2208 "autotranslate_detected_language": ' . (int)$settings['autotranslate_detected_language'] . ',
2209 "always_detect_autotranslated_language": ' . (int)$settings['always_detect_autotranslated_language'] . ',
2210 "widget_text_color": "' . $settings['widget_text_color'] . '",
2211 "show_language_titles": ' . (int)$settings['show_language_titles'] . ',
2212 "enable_dropdown": ' . (int)$settings['enable_dropdown'] . ',
2213 "enable_modal": ' . (int)($settings['enable_modal'] ?? 0) . ',
2214 "equal_widths": ' . (int)$settings['equal_widths'] . ',
2215 "reader_button_position": "' . $settings['reader_button_position'] . '",
2216 "custom_css": "' . addslashes(preg_replace('/\s+/', ' ', str_replace(["\r", "\n"], ' ', (string) $settings['custom_css']))) . '",
2217 "alt_flags": ' . json_encode($alt_flags). ',
2218 "realtime_translations_retrigger_events": ' . json_encode($settings['realtime_translations_retrigger_events']) . ',
2219 "realtime_translations_retrigger_events_delay": ' . (int)$settings['realtime_translations_retrigger_events_delay'] . ',
2220 "switcher_horizontal_position": "' . $switcher_horizontal_position . '",
2221 "switcher_vertical_position": "' . $switcher_vertical_position . '",
2222 "flags_location": "' . esc_js($flagsPath) . '",
2223 "flag_loading": "' . $settings['flag_loading'] . '",
2224 "flag_style": "' . $settings['flag_style'] . '",
2225 "hide_flags": ' . (int)($settings['hide_flags'] ?? 0) . ',
2226 "widget_max_height": ' . (int)$settings['widget_max_height'] . ',
2227 "incremental_enabled": ' . (int)($settings['incremental_enabled'] ?? 0) . ',
2228 "incremental_show_progress_popup": ' . (int)($settings['incremental_show_progress_popup'] ?? 0) . ',
2229 "incremental_caseinsensitive": ' . (int)($settings['incremental_caseinsensitive'] ?? 0) . ',
2230 "incremental_filter_excluded_words": ' . (int)($settings['incremental_filter_excluded_words'] ?? 0) . ',
2231 "incremental_filter_dictionary_keys": ' . (int)($settings['incremental_filter_dictionary_keys'] ?? 0) . ',
2232 "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') . '",
2233 "incremental_excluded_text_patterns": "' . esc_js($settings['incremental_excluded_text_patterns'] ?? '') . '",
2234 "incremental_preserve_new_texts": ' . (int)($settings['incremental_preserve_new_texts'] ?? 1) . '
2235 };');
2236
2237 $languageStringsScript = '';
2238
2239 // Generic translations
2240 $labels = [
2241 'TRANSLATING',
2242 'TRANSLATING_WAIT',
2243 'TRANSLATING_COMPLETE',
2244 'READING_INPROGRESS',
2245 'READING_END',
2246 'READING_EMPTY',
2247 'CHOOSE_LANGUAGE'
2248 ];
2249
2250 foreach ($labels as $label) {
2251 $languageStringsScript .= 'var PLG_GPTRANSLATE_' . $label . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_' . $label)) . '";' . PHP_EOL;
2252 }
2253
2254 $languages = [
2255 'AF', 'SQ', 'AM', 'AR', 'HY', 'AZ', 'EU', 'BE', 'BN', 'BS', 'BG', 'CA', 'CEB', 'NY',
2256 'ZH', 'CO', 'HR', 'CS', 'DA', 'NL', 'EN', 'EO', 'ET', 'TL', 'FI', 'FR',
2257 'FY', 'GL', 'KA', 'DE', 'EL', 'GU', 'HT', 'HA', 'HAW', 'IW', 'HI', 'HMN', 'HU',
2258 'IS', 'IG', 'ID', 'GA', 'IT', 'JA', 'JW', 'KN', 'KK', 'KL', 'KM', 'KO', 'KU', 'KY', 'LO',
2259 'LA', 'LV', 'LT', 'LB', 'MK', 'MG', 'MS', 'ML', 'MT', 'MI', 'MR', 'MN', 'MY', 'NE',
2260 'NO', 'PS', 'FA', 'PL', 'PT', 'PA', 'RO', 'RU', 'SM', 'GD', 'SR', 'ST', 'SN', 'SD',
2261 'SI', 'SK', 'SL', 'SO', 'ES', 'SU', 'SW', 'SV', 'TG', 'TA', 'TE', 'TH', 'TR', 'UK',
2262 'UR', 'UZ', 'VI', 'CY', 'XH', 'YI', 'YO', 'ZU', 'ZT'
2263 ];
2264
2265
2266 foreach ($languages as $lang) {
2267 $languageStringsScript .= 'var PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang)) . '";' . PHP_EOL;
2268 }
2269
2270 wp_register_script('gptranslate-js-language-strings', '', [], $this->version, true);
2271 wp_enqueue_script('gptranslate-js-language-strings');
2272 wp_add_inline_script('gptranslate-js-language-strings', $languageStringsScript);
2273
2274
2275 // Dictionary
2276 $words_leafnodes_excluded_bylanguage_repeatable = $settings['words_leafnodes_excluded_bylanguage_repeatable'];
2277 if ($words_leafnodes_excluded_bylanguage_repeatable) {
2278 if (is_string($words_leafnodes_excluded_bylanguage_repeatable)) {
2279 $words_leafnodes_excluded_bylanguage_repeatable = json_decode($words_leafnodes_excluded_bylanguage_repeatable, true);
2280 }
2281
2282 // Ora convertiamo l'array normale nel formato con chiavi tipo words_leafnodes_excluded_bylanguage_repeatable0, 1, 2...
2283 $formatted = [];
2284 foreach ($words_leafnodes_excluded_bylanguage_repeatable as $index => $row) {
2285 $formatted["words_leafnodes_excluded_bylanguage_repeatable{$index}"] = [
2286 'words_leafnodes_excluded_bylanguage' => rtrim($row['word'] ?? '', ', '),
2287 'words_leafnodes_excluded_bylanguage_language_original' => $row['langOriginal'] ?? '*',
2288 'words_leafnodes_excluded_bylanguage_language_target' => $row['langTranslated'] ?? '*',
2289 'words_leafnodes_excluded_bylanguage_translation' => rtrim($row['optionalTranslation'] ?? '', ', ')
2290 ];
2291 }
2292
2293 // Correctly formatted JSON encode
2294 $formatted_json = json_encode($formatted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
2295
2296 // Inietto i dati PRIMA che venga eseguito gptranslate.js
2297 wp_register_script('gptranslate-js-word-leafones-excluded-language', '', [], $this->version, true);
2298 wp_enqueue_script('gptranslate-js-word-leafones-excluded-language');
2299 wp_add_inline_script(
2300 'gptranslate-js-word-leafones-excluded-language',
2301 'var chatgptWordsLeafnodesExcludedByLanguage = ' . $formatted_json . ';'
2302 );
2303 }
2304
2305 // Local or remote script
2306 if($settings['proxy_responsive_loading_script'] == 1) {
2307 wp_enqueue_script('gptranslate-responsivevoice', plugin_dir_url(__FILE__) . 'assets/js/responsivevoice.js', [], $this->version, true);
2308 } else {
2309 wp_enqueue_script('gptranslate-responsivevoice', 'https://code.responsivevoice.org/responsivevoice.js?key=' . $settings ['responsivevoice_apikey'], [], $this->version, true);
2310 }
2311
2312 wp_enqueue_script('gptranslate-jsonrepair', plugin_dir_url(__FILE__) . 'assets/js/jsonrepair/index.js', [], $this->version, true);
2313 $gptranslateMainScriptMode = 1;
2314 if (isset($settings['main_script_integrity_check'])) {
2315 $gptranslateMainScriptMode = (int) $settings['main_script_integrity_check'];
2316 }
2317 $gptranslateMainScriptLocalFile = plugin_dir_path(__FILE__) . 'assets/js/gptranslate.js';
2318 $gptranslateMainScriptUrl = plugin_dir_url(__FILE__) . 'assets/js/gptranslate.js';
2319 $gptranslateMainScriptRemoteUrl = 'https://storejextensions.org/cdn/gptranslate/wordpress/gptranslate.js';
2320 $gptranslateMainScriptEmpty = file_exists($gptranslateMainScriptLocalFile) && (int) @filesize($gptranslateMainScriptLocalFile) === 0;
2321 if ($gptranslateMainScriptEmpty && $gptranslateMainScriptMode == 1) {
2322 $response = wp_remote_get($gptranslateMainScriptRemoteUrl, [
2323 'timeout' => 8,
2324 'redirection' => 2,
2325 ]);
2326 if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
2327 $body = wp_remote_retrieve_body($response);
2328 if (is_string($body) && strlen($body) > 1024 && is_writable(dirname($gptranslateMainScriptLocalFile))) {
2329 file_put_contents($gptranslateMainScriptLocalFile, $body, LOCK_EX);
2330 }
2331 }
2332 } elseif ($gptranslateMainScriptEmpty && $gptranslateMainScriptMode == 2) {
2333 $gptranslateMainScriptUrl = $gptranslateMainScriptRemoteUrl;
2334 }
2335 wp_enqueue_script('gptranslate-main', $gptranslateMainScriptUrl, [], $this->version, true);
2336
2337 // Enqueue Bootstrap component
2338 if(!$settings['disable_bootstrap_css']) {
2339 wp_enqueue_script('gptranslate-bstoast', plugin_dir_url(__FILE__) . 'assets/js/toast.min.js', [], $this->version, true);
2340 wp_enqueue_style(
2341 'bootstrap-css',
2342 plugin_dir_url(__FILE__) . 'assets/css/bootstrap.min.css',
2343 [],
2344 '5.3.2'
2345 );
2346 } else {
2347 // Add custom CSS only to replicate the toast and progress styles of Bootstrap
2348 wp_register_style('gptranslate-bootstrap-style', false, [], $this->version);
2349 wp_enqueue_style('gptranslate-bootstrap-style');
2350 wp_add_inline_style('gptranslate-bootstrap-style', '
2351 .progress-gptranslate,.progress-gptranslate-reading{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:0.25rem}
2352 .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}
2353 .progress-gptranslate .toast.show,.progress-gptranslate-reading .toast.show{display:block}
2354 .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)}
2355 .progress-gptranslate .toast-body,.progress-gptranslate-reading .toast-body{padding:0.75rem}
2356 .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}
2357 .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}
2358 .progress-gptranslate .progress-bar-animated,.progress-gptranslate-reading .progress-bar-animated{animation:progress-bar-stripes-gptranslate 1s linear infinite}
2359 @keyframes progress-bar-stripes-gptranslate{0%{background-position-x:1rem}}
2360 .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}
2361 .progress-gptranslate .btn-close:hover,.progress-gptranslate-reading .btn-close:hover{color:#000;text-decoration:none;opacity:0.75}
2362 .progress-gptranslate .me-auto,.progress-gptranslate-reading .me-auto{margin-right:auto!important}
2363 html[dir="rtl"] .progress-gptranslate .me-auto,html[dir="rtl"] .progress-gptranslate-reading .me-auto{margin-right:unset !important;margin-left:auto!important}
2364 .progress-gptranslate .text-muted,.progress-gptranslate-reading .text-muted{color:#6c757d!important}
2365 .progress-gptranslate .bg-primary,.progress-gptranslate-reading .bg-primary{background-color:#0d6efd!important}
2366 .progress-gptranslate .bg-secondary,.progress-gptranslate-reading .bg-secondary{background-color:#6c757d!important}
2367 .progress-gptranslate .bg-success,.progress-gptranslate-reading .bg-success{background-color:#198754!important}
2368 .progress-gptranslate .bg-danger,.progress-gptranslate-reading .bg-danger{background-color:#dc3545!important}
2369 .progress-gptranslate .bg-warning,.progress-gptranslate-reading .bg-warning{background-color:#ffc107!important;color:#000!important}
2370 .progress-gptranslate .bg-info,.progress-gptranslate-reading .bg-info{background-color:#0dcaf0!important;color:#000!important}
2371 .progress-gptranslate .bg-light,.progress-gptranslate-reading .bg-light{background-color:#f8f9fa!important;color:#000!important}
2372 .progress-gptranslate .bg-dark,.progress-gptranslate-reading .bg-dark{background-color:#212529!important}
2373 ');
2374
2375 // Closer for the toast element
2376 wp_register_script('gptranslate-toast-dismiss', false, [], $this->version, true);
2377 wp_enqueue_script('gptranslate-toast-dismiss');
2378 wp_add_inline_script('gptranslate-toast-dismiss', '
2379 document.addEventListener("DOMContentLoaded", function() {
2380 document.addEventListener("click", function(e) {
2381 if (e.target.matches(".btn-close[data-bs-dismiss=\"toast\"]") ||
2382 e.target.closest(".btn-close[data-bs-dismiss=\"toast\"]")) {
2383 const btnClose = e.target.matches(".btn-close") ? e.target : e.target.closest(".btn-close");
2384 const toast = btnClose.closest(".toast");
2385 if (toast) {
2386 toast.classList.remove("show");
2387 const progressContainer = toast.closest(".progress-gptranslate, .progress-gptranslate-reading");
2388 if (progressContainer) {
2389 progressContainer.remove();
2390 }
2391 }
2392 }
2393 });
2394 });
2395 ');
2396 }
2397
2398 // Registra un handle CSS vuoto se necessario
2399 wp_register_style('gptranslate-dynamic-css', false, [], $this->version);
2400 wp_enqueue_style('gptranslate-dynamic-css');
2401
2402 // Prepara lo stile dinamico
2403 $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') . '; }' .
2404 '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; }' .
2405 'div.gpt_float_switcher { border-radius: ' . intval($settings['popup_border_radius']) . 'px; }' .
2406 'div.gpt_float_switcher img, svg.svg-inline--fa { box-sizing: border-box; width: ' . intval($settings['popup_iconsize']) . 'px; }';
2407
2408 if (!empty($settings['disable_toast_popups']) && $settings['disable_toast_popups'] == 1) {
2409 $dynamic_css .= '.progress.progress-gptranslate,.progress.progress-gptranslate-reading{ display: none !important; }';
2410 }
2411
2412 if (!empty($settings['hide_flags']) && $settings['hide_flags'] == 1) {
2413 $dynamic_css .= '.gptranslate-flag{display:none!important}';
2414 }
2415
2416 // Opacity del background widget (solo se diverso da 1.0)
2417 if (!empty($settings['widget_opacity']) && floatval($settings['widget_opacity']) != 1.0) {
2418 $bgColor = !empty($settings['widget_background_color']) ? esc_attr($settings['widget_background_color']) : '#FFFFFF';
2419 $opacity = floatval($settings['widget_opacity']);
2420 $alphaHex = str_pad(dechex(round($opacity * 255)), 2, '0', STR_PAD_LEFT);
2421 $bgColorWithAlpha = $bgColor . strtoupper($alphaHex);
2422
2423 $dynamic_css .= 'div.gpt_float_switcher .gt-selected, div.gpt_float_switcher, div.gpt_options { background-color: ' . $bgColorWithAlpha . ' !important; }';
2424 }
2425
2426 // Inietta il CSS inline
2427 wp_add_inline_style('gptranslate-dynamic-css', $dynamic_css);
2428
2429 // --- Load theme RTL stylesheet if available ---
2430 if ( ! empty( $settings['auto_set_language_direction'] ) && is_rtl() ) {
2431 // Common file names
2432 $rtl_candidates = array(
2433 get_stylesheet_directory() . '/style-rtl.css',
2434 get_stylesheet_directory() . '/rtl.css'
2435 );
2436
2437 $rtl_file = '';
2438 foreach ( $rtl_candidates as $candidate ) {
2439 if ( file_exists( $candidate ) ) {
2440 $rtl_file = $candidate;
2441 break;
2442 }
2443 }
2444
2445 if ( $rtl_file ) {
2446 $rtl_uri = str_replace(
2447 get_stylesheet_directory(),
2448 get_stylesheet_directory_uri(),
2449 $rtl_file
2450 );
2451
2452 wp_enqueue_style(
2453 'theme-rtl',
2454 $rtl_uri,
2455 array(),
2456 filemtime( $rtl_file )
2457 );
2458 }
2459 }
2460 }
2461 }
2462
2463 /**
2464 * Redirect to Wizard only once on fresh install:
2465 * first click on GPTranslate Translations/Settings opens Wizard, then never forced again.
2466 */
2467 add_action('admin_init', function() {
2468 if (!is_admin() || !current_user_can('manage_options')) {
2469 return;
2470 }
2471 if (wp_doing_ajax()) {
2472 return;
2473 }
2474 $page = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2475 if ($page !== 'gptranslate' && $page !== 'gptranslate-settings') {
2476 return;
2477 }
2478 // Fresh install only (option explicitly set to 0 at first install)
2479 if ((int)get_option('gptranslate_wizard_completed', -1) !== 0) {
2480 return;
2481 }
2482 // Force only once
2483 if ((int)get_option('gptranslate_wizard_first_redirect_done', 0) === 1) {
2484 return;
2485 }
2486 update_option('gptranslate_wizard_first_redirect_done', 1, false);
2487 wp_safe_redirect(admin_url('admin.php?page=gptranslate-wizard'));
2488 exit;
2489 });
2490
2491 // Force WordPress.org update check on plugin activation
2492 register_activation_hook( __FILE__, function() {
2493 if ( function_exists('wp_update_plugins') ) {
2494 wp_update_plugins();
2495 }
2496 });
2497
2498 // Schedule a daily update check
2499 add_action( 'gptranslate_daily_update_check', function() {
2500 if ( function_exists('wp_update_plugins') ) {
2501 wp_update_plugins();
2502 }
2503 });
2504
2505 if ( ! wp_next_scheduled( 'gptranslate_daily_update_check' ) ) {
2506 wp_schedule_event( time(), 'daily', 'gptranslate_daily_update_check' );
2507 }
2508
2509 // 🧹 Cleanup scheduled event on deactivation
2510 register_deactivation_hook( __FILE__, function() {
2511 wp_clear_scheduled_hook( 'gptranslate_daily_update_check' );
2512 });
2513
2514 /**
2515 * Add main admin scripts for example to manage records add/delete functions
2516 *
2517 * @access public
2518 */
2519 add_action('admin_enqueue_scripts', function() {
2520 if(isset($_GET['page']) && strpos(sanitize_key($_GET['page']), 'gptranslate') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2521 $currentPage = sanitize_key($_GET['page']);
2522 // Enqueue JS
2523 wp_enqueue_script ( 'gptranslate-js', plugin_dir_url ( __FILE__ ) . 'assets/js/admin.js', [ ], GPTranslate::$pluginVersion, true );
2524 wp_enqueue_script ( 'gptranslate-js-select2', plugin_dir_url ( __FILE__ ) . 'assets/js/select2.min.js', [ 'jquery' ], GPTranslate::$pluginVersion, true );
2525
2526 if ($currentPage === 'gptranslate-wizard') {
2527 wp_enqueue_script('gptranslate-wizard-js', plugin_dir_url(__FILE__) . 'assets/js/wizard.js', [], GPTranslate::$pluginVersion, true);
2528 wp_enqueue_style('gptranslate-wizard-css', plugin_dir_url(__FILE__) . 'assets/css/wizard.css', [], GPTranslate::$pluginVersion);
2529 wp_add_inline_script('gptranslate-wizard-js', '
2530 const PLG_GPTRANSLATE_WIZARD_SAVE_ERROR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_SAVE_ERROR')) . '";
2531 const PLG_GPTRANSLATE_WIZARD_NETWORK_ERROR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_NETWORK_ERROR')) . '";
2532 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP1 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP1')) . '";
2533 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP2 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP2')) . '";
2534 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP3 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP3')) . '";
2535 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP4 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP4')) . '";
2536 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP5 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP5')) . '";
2537 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP6 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP6')) . '";
2538 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP7 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP7')) . '";
2539 const PLG_GPTRANSLATE_WIZARD_DONE_MODEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_DONE_MODEL')) . '";
2540 const PLG_GPTRANSLATE_WIZARD_DONE_TRANSLATION_METHOD = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_DONE_TRANSLATION_METHOD')) . '";
2541 ', 'before');
2542 }
2543
2544 if($currentPage != 'gptranslate-settings' && $currentPage != 'gptranslate-wizard' && !isset($_GET['action'])) {
2545 wp_enqueue_script ( 'crawler-js', plugin_dir_url ( __FILE__ ) . 'assets/js/crawler.js', [ ], GPTranslate::$pluginVersion, true );
2546 }
2547
2548 // Enqueue CSS
2549 wp_enqueue_style ( 'gptranslate-css', plugin_dir_url ( __FILE__ ) . 'assets/css/admin.css', [ ], GPTranslate::$pluginVersion );
2550 wp_enqueue_style ( 'gptranslate-css-select2', plugin_dir_url ( __FILE__ ) . 'assets/css/select2.min.css', [ ], GPTranslate::$pluginVersion );
2551
2552 if($currentPage != 'gptranslate-settings' && $currentPage != 'gptranslate-wizard' && !isset($_GET['action'])) {
2553 wp_enqueue_style ( 'crawler-css', plugin_dir_url ( __FILE__ ) . 'assets/css/crawler.css', [ ], GPTranslate::$pluginVersion );
2554 }
2555
2556 wp_localize_script('gptranslate-js', 'gptranslate_vars', [
2557 'ajaxurl' => admin_url('admin-ajax.php'),
2558 'nonce' => wp_create_nonce('gptranslate_migrate_translations'),
2559 'deletenonce' => wp_create_nonce('gptranslate_delete_translations'),
2560 'gptranslateNonce' => wp_create_nonce('gptranslate_crawler_nonce'),
2561 'testApikeyNonce' => wp_create_nonce('gptranslate_test_apikey'),
2562 'wizardNonce' => wp_create_nonce('gptranslate_wizard_nonce'),
2563 'wizardFirstRun' => (get_option('gptranslate_wizard_completed', null) === 0) ? 1 : 0,
2564 'gptApiKey' => gptranslate_get_api_secret(),
2565 'restNonce' => wp_create_nonce('wp_rest'),
2566 'i18n_test_apikey' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY'),
2567 'i18n_test_apikey_testing' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_TESTING'),
2568 'i18n_test_apikey_success' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_SUCCESS'),
2569 'i18n_test_apikey_error' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_ERROR'),
2570 'i18n_test_apikey_empty' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_EMPTY'),
2571 'i18n_apikey_required' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_APIKEY_REQUIRED')
2572 ]);
2573 } else {
2574 // Not on a GPTranslate page - check if there's an auto-crawl queue for this user
2575 $auto_crawl_queue = get_transient('gpt_auto_crawl_queue_' . get_current_user_id());
2576 if ($auto_crawl_queue && !empty($auto_crawl_queue['url'])) {
2577 // Get settings needed for crawler globals
2578 $settings = get_option('gptranslate_options', []);
2579
2580 // Verify that auto_crawl_on_publish is still enabled
2581 $auto_crawl_enabled = isset($settings['auto_crawl_on_publish']) && ($settings['auto_crawl_on_publish'] == 1 || $settings['auto_crawl_on_publish'] === '1');
2582 if (!$auto_crawl_enabled) {
2583 // Setting has been disabled, clear the queue and exit
2584 delete_transient('gpt_auto_crawl_queue_' . get_current_user_id());
2585 return;
2586 }
2587
2588 // Enqueue crawler.js if not already enqueued
2589 if (!wp_script_is('crawler-js', 'enqueued')) {
2590 // Add inline script with ALL global variables and constants that crawler.js needs
2591 wp_add_inline_script('jquery', '
2592 const PLG_GPTRANSLATE_CRAWLER_TARGET_LINK = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_TARGET_LINK')) . '";
2593 const PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK')) . '";
2594 const PLG_GPTRANSLATE_CRAWLER_START = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_START')) . '";
2595 const PLG_GPTRANSLATE_CRAWLER_START_DESC = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_START_DESC')) . '";
2596 const PLG_GPTRANSLATE_CRAWLER_STARTED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STARTED')) . '";
2597 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING')) . '";
2598 const PLG_GPTRANSLATE_CRAWLER_FOOTER = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_FOOTER')) . '";
2599 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE')) . '";
2600 const PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED')) . '";
2601 const PLG_GPTRANSLATE_CRAWLER_STOP = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOP')) . '";
2602 const PLG_GPTRANSLATE_CRAWLER_STOP_DESC = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOP_DESC')) . '";
2603 const PLG_GPTRANSLATE_CRAWLER_STARTING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STARTING')) . '";
2604 const PLG_GPTRANSLATE_CRAWLER_STOPPING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOPPING')) . '";
2605 const PLG_GPTRANSLATE_CRAWLER_STOPPED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOPPED')) . '";
2606 const PLG_GPTRANSLATE_CRAWLER_NO_SWITCHER_DETECTED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_NO_SWITCHER_DETECTED')) . '";
2607 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
2608 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
2609 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
2610 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
2611 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
2612 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
2613 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
2614 const PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION')) . '";
2615 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL')) . '";
2616 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL')) . '";
2617 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES')) . '";
2618 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED')) . '";
2619 const PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED')) . '";
2620 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION')) . '";
2621 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER')) . '";
2622 var gptranslateBaseCrawlerHome = "' . esc_js(trailingslashit(get_site_url())) . '";
2623 var gptranslateSitemapUrl = "' . esc_js(rest_url('gptranslate/v1/sitemap.xml')) . '";
2624 var gptranslateDefaultLanguage = "' . esc_js($settings['language'] ?? 'en') . '";
2625 var gptranslateCrawlerTimeout = "' . (isset($settings['crawler_timeout']) ? (int)$settings['crawler_timeout'] : '30') . '";
2626 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", ($settings['crawler_exclusions'] ?? ''))), ',')) . '";
2627 var gptranslateCrawlerSkipTranslated = ' . (int)($settings['crawler_skip_translated'] ?? 0) . ';
2628 var gptSubfolderInstallation = ' . (int)($settings['subfolder_installation'] ?? 0) . ';
2629 var gptranslateRewriteLanguageUrl = ' . (int)($settings['rewrite_language_url'] ?? 0) . ';
2630 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
2631 var gptVersionNumeric = 1;
2632 var gptranslateSitemapAvailableLanguages = ' . wp_json_encode(gptranslate_get_available_languages()) . ';
2633 ', 'before');
2634
2635 wp_enqueue_script('crawler-js', plugin_dir_url(__FILE__) . 'assets/js/crawler.js', ['jquery'], GPTranslate::$pluginVersion, true);
2636
2637 // Localize gptranslate_vars for crawler.js
2638 wp_localize_script('crawler-js', 'gptranslate_vars', [
2639 'ajaxurl' => admin_url('admin-ajax.php'),
2640 'gptranslateNonce' => wp_create_nonce('gptranslate_crawler_nonce'),
2641 'gptApiKey' => gptranslate_get_api_secret(),
2642 'restNonce' => wp_create_nonce('wp_rest'),
2643 ]);
2644 }
2645
2646 // Enqueue auto-crawler.js
2647 wp_enqueue_script('auto-crawler-js', plugin_dir_url(__FILE__) . 'assets/js/auto-crawler.js', ['crawler-js'], GPTranslate::$pluginVersion, true);
2648
2649 // Pass the post URL and nonce to auto-crawler.js
2650 wp_localize_script('auto-crawler-js', 'gptAutoCrawlerData', [
2651 'url' => $auto_crawl_queue['url'],
2652 'nonce' => wp_create_nonce('gpt_auto_crawl_nonce')
2653 ]);
2654 }
2655 }
2656 });
2657
2658 // ============================================================================
2659 // AJAX handler for toggling server-side translations during crawler execution
2660 // This prevents conflicts between crawler and server-side translation system
2661 // ============================================================================
2662 add_action('wp_ajax_gptranslate_toggle_serverside', 'gptranslate_toggle_serverside_handler');
2663
2664 /**
2665 * AJAX handler to disable/restore server-side translations during crawler
2666 *
2667 * Actions:
2668 * - 'check': Check current state and disable if enabled
2669 * - 'restore': Restore previous state if it was enabled
2670 *
2671 * This ensures crawler operates without interference from server-side translations
2672 * and automatically restores the original state when crawler stops.
2673 */
2674 function gptranslate_toggle_serverside_handler() {
2675 // Verify nonce for security
2676 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'gptranslate_crawler_nonce')) {
2677 wp_send_json_error(array('message' => 'Invalid security token'));
2678 return;
2679 }
2680
2681 // Verify admin permissions
2682 if (!current_user_can('manage_options')) {
2683 wp_send_json_error(array('message' => 'Unauthorized access'));
2684 return;
2685 }
2686
2687 // Get the action: 'check' or 'restore'
2688 $toggle_action = isset($_POST['toggle_action']) ? sanitize_text_field($_POST['toggle_action']) : '';
2689
2690 if ($toggle_action !== 'check' && $toggle_action !== 'restore') {
2691 wp_send_json_error(array('message' => 'Invalid action parameter'));
2692 return;
2693 }
2694
2695 // Get plugin options from database
2696 // NOTE: Verify this option name matches your actual plugin option name
2697 $option_name = 'gptranslate_options';
2698 $options = get_option($option_name, array());
2699
2700 if (!is_array($options)) {
2701 $options = array();
2702 }
2703
2704 try {
2705 if ($toggle_action === 'check') {
2706 // ACTION CHECK: Check if server-side translations are enabled and disable if necessary
2707
2708 // Get current value
2709 $current_value = isset($options['serverside_translations']) ? $options['serverside_translations'] : '0';
2710
2711 if ($current_value === '1') {
2712 // Server-side translations are enabled, disable them temporarily
2713 $options['serverside_translations'] = '0';
2714
2715 // Update options in database
2716 update_option($option_name, $options);
2717
2718 $message = 'Server-side translations were enabled and have been disabled for crawler';
2719 $action_taken = 'disabled';
2720 } else {
2721 // Already disabled, no action needed
2722 $message = 'Server-side translations were already disabled, no action needed';
2723 $action_taken = 'none';
2724 }
2725
2726 // Return response with original state
2727 wp_send_json_success(array(
2728 'message' => $message,
2729 'were_enabled' => $current_value,
2730 'action_taken' => $action_taken
2731 ));
2732
2733 } else if ($toggle_action === 'restore') {
2734 // ACTION RESTORE: Re-enable only if client tells us they were enabled before
2735
2736 $were_enabled = isset($_POST['were_enabled']) ? sanitize_text_field($_POST['were_enabled']) : '0';
2737
2738 if ($were_enabled === '1') {
2739 // They were enabled before crawler, restore them
2740 $options['serverside_translations'] = '1';
2741
2742 // Update options in database
2743 update_option($option_name, $options);
2744
2745 $message = 'Server-side translations have been restored to enabled';
2746 $action_taken = 'restored';
2747 } else {
2748 // They were not enabled, no action needed
2749 $message = 'Server-side translations were not enabled before, no action needed';
2750 $action_taken = 'none';
2751 }
2752
2753 // Return response
2754 wp_send_json_success(array(
2755 'message' => $message,
2756 'action_taken' => $action_taken
2757 ));
2758 }
2759
2760 } catch (Exception $e) {
2761 wp_send_json_error(array('message' => 'Error: ' . $e->getMessage()));
2762 }
2763 }
2764
2765 // ============================================================================
2766 // AJAX handler for wizard management
2767 // ============================================================================
2768 add_action('wp_ajax_gptranslate_complete_wizard', function () {
2769 check_ajax_referer('gptranslate_wizard_nonce', 'nonce');
2770 if (!current_user_can('manage_options')) {
2771 wp_send_json_error('Unauthorized');
2772 }
2773 update_option('gptranslate_wizard_completed', 1, false);
2774 wp_send_json_success(['completed' => 1]);
2775 });
2776
2777 add_action('wp_ajax_gptranslate_save_wizard', function () {
2778 check_ajax_referer('gptranslate_wizard_nonce', 'nonce');
2779 if (!current_user_can('manage_options')) {
2780 wp_send_json_error('Unauthorized');
2781 }
2782 $opts = get_option('gptranslate_options', []);
2783 $opts['language'] = isset($_POST['language']) ? sanitize_text_field(wp_unslash($_POST['language'])) : ($opts['language'] ?? 'en');
2784 $sourceLang = strtolower((string) $opts['language']);
2785 $rawEnabled = isset($_POST['enabled_languages']) ? wp_unslash($_POST['enabled_languages']) : '[]';
2786 $decodedEnabled = json_decode((string) $rawEnabled, true);
2787 $enabledLangs = [];
2788 if (is_array($decodedEnabled)) {
2789 foreach ($decodedEnabled as $lang) {
2790 $lang = strtolower(sanitize_text_field((string) $lang));
2791 if ($lang !== '') {
2792 $enabledLangs[] = $lang;
2793 }
2794 }
2795 }
2796 $opts['languages'] = array_values(array_unique(array_merge([$sourceLang], $enabledLangs)));
2797 $opts['google_translate_engine'] = isset($_POST['engine']) ? sanitize_text_field(wp_unslash($_POST['engine'])) : ($opts['google_translate_engine'] ?? '1');
2798 $opts['chatgpt_model'] = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : ($opts['chatgpt_model'] ?? 'gpt-5.5');
2799 $opts['google_translate_method'] = isset($_POST['google_method']) ? sanitize_text_field(wp_unslash($_POST['google_method'])) : ($opts['google_translate_method'] ?? '0');
2800 $opts['chatgpt_apikey'] = isset($_POST['apikey']) ? sanitize_text_field(wp_unslash($_POST['apikey'])) : ($opts['chatgpt_apikey'] ?? '');
2801 $opts['serverside_translations'] = isset($_POST['serverside']) ? sanitize_text_field(wp_unslash($_POST['serverside'])) : ($opts['serverside_translations'] ?? '1');
2802 $opts['serverside_translations_method'] = isset($_POST['serverside_method']) ? sanitize_text_field(wp_unslash($_POST['serverside_method'])) : ($opts['serverside_translations_method'] ?? 'regex');
2803 // Keep wizard behavior aligned with main settings: enabling server-side translations
2804 // also enables language detection + autotranslate detected language.
2805 if ((string) $opts['serverside_translations'] === '1') {
2806 $opts['detect_current_language'] = '1';
2807 $opts['autotranslate_detected_language'] = '1';
2808 }
2809 $opts['translate_metadata'] = isset($_POST['metadata']) ? sanitize_text_field(wp_unslash($_POST['metadata'])) : ($opts['translate_metadata'] ?? '0');
2810 $opts['add_canonical'] = isset($_POST['add_canonical']) ? sanitize_text_field(wp_unslash($_POST['add_canonical'])) : ($opts['add_canonical'] ?? '0');
2811 $opts['add_alternate'] = isset($_POST['add_alternate']) ? sanitize_text_field(wp_unslash($_POST['add_alternate'])) : ($opts['add_alternate'] ?? '0');
2812 $opts['set_html_lang'] = isset($_POST['set_html_lang']) ? sanitize_text_field(wp_unslash($_POST['set_html_lang'])) : ($opts['set_html_lang'] ?? '0');
2813 $opts['rewrite_language_url'] = isset($_POST['rewrite_language_url']) ? sanitize_text_field(wp_unslash($_POST['rewrite_language_url'])) : ($opts['rewrite_language_url'] ?? '0');
2814 $opts['rewrite_language_alias'] = isset($_POST['rewrite_language_alias']) ? sanitize_text_field(wp_unslash($_POST['rewrite_language_alias'])) : ($opts['rewrite_language_alias'] ?? '0');
2815 $opts['rewrite_page_links'] = isset($_POST['rewrite_page_links']) ? sanitize_text_field(wp_unslash($_POST['rewrite_page_links'])) : ($opts['rewrite_page_links'] ?? '0');
2816 $opts['float_position'] = isset($_POST['position']) ? sanitize_text_field(wp_unslash($_POST['position'])) : ($opts['float_position'] ?? 'bottom-left');
2817 update_option('gptranslate_options', $opts);
2818 update_option('gptranslate_wizard_completed', 1, false);
2819 wp_send_json_success(['saved' => 1]);
2820 });
2821
2822 // ============================================================================
2823 // AJAX handler for getting already-translated URLs (crawler fast skip)
2824 // ============================================================================
2825 add_action('wp_ajax_gptranslate_get_translated_urls', function () {
2826 check_ajax_referer('gptranslate_crawler_nonce', 'nonce');
2827 if (!current_user_can('manage_options')) {
2828 wp_send_json_error(array('message' => 'Unauthorized'));
2829 return;
2830 }
2831 global $wpdb;
2832 $table = $wpdb->prefix . 'gptranslate';
2833
2834 // Get enabled target languages (excluding the default/source language)
2835 $opts = get_option('gptranslate_options', array());
2836 $defaultLanguage = strtolower($opts['language'] ?? 'en');
2837 $enabledLanguages = array_map('strtolower', $opts['languages'] ?? array());
2838 $targetLanguages = array_values(array_filter($enabledLanguages, function($lang) use ($defaultLanguage) {
2839 return $lang !== $defaultLanguage;
2840 }));
2841 $numTargetLanguages = count($targetLanguages);
2842
2843 if ($numTargetLanguages === 0) {
2844 wp_send_json_success(array('urls' => array()));
2845 return;
2846 }
2847
2848 // Fetch all published rows: pagelink + languagetranslated
2849 // pagelink already contains the language prefix (e.g. http://site/fr/page/)
2850 // so we normalize it in PHP to group by the original URL
2851 $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2852 "SELECT pagelink, languagetranslated FROM {$table} WHERE published = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2853 ARRAY_A
2854 );
2855
2856 // Helper: strip language prefix from URL path
2857 $stripLangPrefix = function($url) {
2858 $cleaned = rtrim($url, '/');
2859 $parsedUrl = parse_url($cleaned);
2860 if ($parsedUrl === false) {
2861 return $cleaned;
2862 }
2863 $pathname = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
2864 if (preg_match('#^/([a-z]{2}(-[a-z]{2})?)(?=/|$)#i', $pathname)) {
2865 $pathname = preg_replace('#^/[a-z]{2}(-[a-z]{2})?#i', '', $pathname);
2866 if (empty($pathname)) {
2867 $pathname = '/';
2868 }
2869 }
2870 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
2871 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
2872 $port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
2873 return $scheme . $host . $port . $pathname;
2874 };
2875
2876 // Group by normalized URL → collect translated languages
2877 $urlLangMap = array();
2878 foreach ($rows as $row) {
2879 $normalizedUrl = $stripLangPrefix($row['pagelink']);
2880 $lang = strtolower($row['languagetranslated']);
2881 if (!isset($urlLangMap[$normalizedUrl])) {
2882 $urlLangMap[$normalizedUrl] = array();
2883 }
2884 if (!in_array($lang, $urlLangMap[$normalizedUrl])) {
2885 $urlLangMap[$normalizedUrl][] = $lang;
2886 }
2887 }
2888
2889 // Keep only URLs where ALL target languages are present
2890 $fullyTranslatedUrls = array();
2891 foreach ($urlLangMap as $normalizedUrl => $translatedLangs) {
2892 $missing = array_diff($targetLanguages, $translatedLangs);
2893 if (empty($missing)) {
2894 $fullyTranslatedUrls[] = $normalizedUrl;
2895 }
2896 }
2897
2898 wp_send_json_success(array('urls' => $fullyTranslatedUrls));
2899 });
2900
2901 // ============================================================================
2902 // AJAX handler for testing API key validity
2903 // ============================================================================
2904 add_action('wp_ajax_gptranslate_test_apikey', function () {
2905 // Verify nonce
2906 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'gptranslate_test_apikey')) {
2907 wp_send_json_error(array('message' => 'Invalid security token'));
2908 return;
2909 }
2910
2911 // Verify admin permissions
2912 if (!current_user_can('manage_options')) {
2913 wp_send_json_error(array('message' => 'Unauthorized access'));
2914 return;
2915 }
2916
2917 $apiKey = isset($_POST['apikey']) ? sanitize_text_field(wp_unslash($_POST['apikey'])) : '';
2918 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
2919
2920 if (empty($apiKey)) {
2921 wp_send_json_error(array('message' => 'API key is empty'));
2922 return;
2923 }
2924
2925 $url = '';
2926 $headers = array('Content-Type' => 'application/json');
2927 $body = '';
2928
2929 try {
2930 if (strpos($model, 'gpt-') === 0 || strpos($model, 'grok-') === 0) {
2931 // OpenAI / ChatGPT / Grok
2932 $url = (strpos($model, 'grok-') === 0) ? 'https://api.x.ai/v1/chat/completions' : 'https://api.openai.com/v1/chat/completions';
2933 $headers['Authorization'] = 'Bearer ' . $apiKey;
2934 $useNewTokenParam = (strpos($model, 'gpt-4.1') === 0 || strpos($model, 'gpt-5') === 0);
2935 $tokenParam = $useNewTokenParam ? 'max_completion_tokens' : 'max_tokens';
2936 $body = wp_json_encode(array(
2937 'model' => $model,
2938 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2939 $tokenParam => 5
2940 ));
2941 } elseif (strpos($model, 'deepseek-') === 0) {
2942 // DeepSeek
2943 $url = 'https://api.deepseek.com/v1/chat/completions';
2944 $headers['Authorization'] = 'Bearer ' . $apiKey;
2945 $body = wp_json_encode(array(
2946 'model' => $model,
2947 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2948 'max_tokens' => 5
2949 ));
2950 } elseif (strpos($model, 'gemini-') === 0) {
2951 // Google Gemini
2952 $apiVersion = (strpos($model, '-preview') !== false) ? 'v1beta' : 'v1';
2953 $url = "https://generativelanguage.googleapis.com/{$apiVersion}/models/{$model}:generateContent";
2954 $headers['x-goog-api-key'] = $apiKey;
2955 $body = wp_json_encode(array(
2956 'contents' => array(array('parts' => array(array('text' => 'Hi')))),
2957 'generationConfig' => array('maxOutputTokens' => 5)
2958 ));
2959 } elseif (strpos($model, 'claude-') === 0) {
2960 // Claude / Anthropic
2961 $url = 'https://api.anthropic.com/v1/messages';
2962 $headers['x-api-key'] = $apiKey;
2963 $headers['anthropic-version'] = '2023-06-01';
2964 unset($headers['Authorization']);
2965 $body = wp_json_encode(array(
2966 'model' => $model,
2967 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2968 'max_tokens' => 5
2969 ));
2970 } elseif ($model === 'google-cloud-translation-api') {
2971 // Google Cloud Translation
2972 $url = 'https://translation.googleapis.com/language/translate/v2?key=' . urlencode($apiKey);
2973 $body = wp_json_encode(array(
2974 'q' => array('hello'),
2975 'source' => 'en',
2976 'target' => 'es',
2977 'format' => 'text'
2978 ));
2979 } elseif ($model === 'deepl-api') {
2980 // DeepL - Auto-detect endpoint based on API key type
2981 // Free API keys end with ':fx', paid keys don't have this suffix
2982 $deeplEndpoint = (strpos ( $apiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
2983 $url = $deeplEndpoint . '/v2/translate';
2984 $headers ['Authorization'] = 'DeepL-Auth-Key ' . $apiKey;
2985 $headers ['Content-Type'] = 'application/x-www-form-urlencoded';
2986 // DeepL requires repeated "text" params (not text[0])
2987 $body = 'text=' . urlencode ( 'hello' ) . '&source_lang=EN&target_lang=ES';
2988 } else {
2989 wp_send_json_error(array('message' => 'Unsupported model: ' . $model));
2990 return;
2991 }
2992
2993 $response = wp_remote_post($url, array(
2994 'headers' => $headers,
2995 'body' => $body,
2996 'timeout' => 60
2997 ));
2998
2999 if (is_wp_error($response)) {
3000 wp_send_json_error(array(
3001 'message' => $response->get_error_message(),
3002 'error_code' => 0,
3003 'http_code' => 0
3004 ));
3005 return;
3006 }
3007
3008 $httpCode = wp_remote_retrieve_response_code($response);
3009 $responseBody = wp_remote_retrieve_body($response);
3010
3011 if ($httpCode >= 200 && $httpCode < 300) {
3012 wp_send_json_success(array(
3013 'result' => true,
3014 'http_code' => $httpCode
3015 ));
3016 } else {
3017 $errorMessage = 'HTTP ' . $httpCode;
3018 $decoded = json_decode($responseBody, true);
3019 if ($decoded) {
3020 if (isset($decoded['error']['message'])) {
3021 $errorMessage = $decoded['error']['message'];
3022 } elseif (isset($decoded['error']['status'])) {
3023 $errorMessage = $decoded['error']['status'];
3024 }
3025 // Claude/Anthropic error format: {type: "error", error: {type, message}}
3026 if (isset($decoded['type']) && $decoded['type'] === 'error' && isset($decoded['error']['message'])) {
3027 $errorMessage = $decoded['error']['message'];
3028 }
3029 }
3030 wp_send_json_error(array(
3031 'message' => $errorMessage,
3032 'error_code' => $httpCode,
3033 'http_code' => $httpCode
3034 ));
3035 }
3036 } catch (Exception $e) {
3037 wp_send_json_error(array(
3038 'message' => $e->getMessage(),
3039 'error_code' => 0,
3040 'http_code' => 0
3041 ));
3042 }
3043 });
3044
3045 add_action('wp_ajax_gptranslate_bulk_delete', function () {
3046 if (!current_user_can('manage_options') || !check_ajax_referer('gptranslate_delete_translations', '_wpnonce', false)) {
3047 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3048 }
3049
3050 global $wpdb;
3051 $ids = isset($_POST['gptid']) ? array_map('intval', (array) $_POST['gptid']) : [];
3052
3053 if (empty($ids)) {
3054 wp_send_json_error('No records selected');
3055 }
3056
3057 $table = $wpdb->prefix . 'gptranslate';
3058 $in = implode(',', array_fill(0, count($ids), '%d'));
3059 $sql = "DELETE FROM {$table} WHERE id IN ($in)";
3060 $result = $wpdb->query($wpdb->prepare($sql, ...$ids)); // phpcs:ignore
3061
3062 if ($result === false) {
3063 wp_send_json_error('Database error');
3064 }
3065
3066 wp_send_json_success();
3067 });
3068
3069 // Handle Export CSV
3070 add_action('admin_post_gptranslate_export_translations_csv', function () {
3071 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_csv', 'gptranslate_export_csv_nonce')) {
3072 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3073 }
3074
3075 global $wpdb;
3076 $table = $wpdb->prefix . 'gptranslate';
3077
3078 $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
3079
3080 if (!$records) {
3081 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
3082 }
3083
3084 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3085 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
3086
3087 header('Content-Type: text/csv');
3088 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.csv"');
3089
3090 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
3091 $output = fopen('php://output', 'w');
3092
3093 // Intestazioni CSV
3094 fputcsv($output, array_keys($records[0]), ",", '"', "\\");
3095
3096 foreach ($records as $record) {
3097 fputcsv($output, $record, ",", '"', "\\");
3098 }
3099
3100 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
3101 fclose($output);
3102 exit;
3103 });
3104
3105 // Handle Import CSV
3106 add_action('admin_post_gptranslate_import_translations_csv', function () {
3107 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_csv', 'gptranslate_import_csv_nonce')) {
3108 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3109 }
3110
3111 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
3112 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
3113 }
3114
3115 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3116 if (!is_uploaded_file($tmp_name)) {
3117 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3118 }
3119
3120 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
3121 $file = fopen($tmp_name, 'r');
3122 if (!$file) {
3123 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3124 }
3125
3126 $headers = fgetcsv($file);
3127 if (!$headers) {
3128 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_CSV_FORMAT')));
3129 }
3130
3131 global $wpdb;
3132 $table = $wpdb->prefix . 'gptranslate';
3133
3134 while (($row = fgetcsv($file)) !== false) {
3135 $countHeaders = count($headers);
3136 $countRow = count($row);
3137 // Invalid combine
3138 if($countHeaders != $countRow) {
3139 continue;
3140 }
3141 $record = array_combine($headers, $row);
3142 if (empty($record['pagelink'])) {
3143 continue; // skip if no primary key
3144 }
3145
3146 $pagelink = sanitize_text_field($record['pagelink']);
3147 $exists = $wpdb->get_var($wpdb->prepare(
3148 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
3149 $pagelink, $record['languageoriginal'], $record['languagetranslated']
3150 ));// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3151
3152 $data = [
3153 'translated_alias' => $record['translated_alias'],
3154 'translations' => $record['translations'],
3155 'alt_translations' => $record['alt_translations'],
3156 'languageoriginal' => sanitize_text_field($record['languageoriginal']),
3157 'languagetranslated' => sanitize_text_field($record['languagetranslated']),
3158 'published' => isset($record['published']) ? (int)$record['published'] : 1,
3159 'translate_date' => $record['translate_date'],
3160 'translation_engine' => sanitize_text_field($record['translation_engine']),
3161 ];
3162
3163 if ($exists) {
3164 $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3165 $table,
3166 $data,
3167 ['pagelink' => $pagelink, 'languageoriginal' => sanitize_text_field($record['languageoriginal']), 'languagetranslated' => sanitize_text_field($record['languagetranslated'])],
3168 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s'],
3169 ['%s','%s','%s']
3170 );
3171 } else {
3172 $data['pagelink'] = $pagelink;
3173 $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3174 $table,
3175 $data,
3176 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
3177 );
3178 }
3179 }
3180
3181 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
3182 fclose($file);
3183
3184 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
3185 exit;
3186 });
3187
3188 // Handle Export XLIFF
3189 add_action('admin_post_gptranslate_export_translations_xliff', function () {
3190 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce')) {
3191 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3192 }
3193
3194 global $wpdb;
3195 $table = $wpdb->prefix . 'gptranslate';
3196
3197 $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
3198
3199 if (!$records) {
3200 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
3201 }
3202
3203 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3204 $fileDate = (date_i18n('Y-m-d', strtotime($localDate)));
3205
3206 header('Content-Type: application/xml; charset=utf-8');
3207 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.xliff"');
3208
3209 $xml = new SimpleXMLElement('<xliff/>');
3210 $xml->addAttribute('version', '1.2');
3211
3212 foreach ($records as $record) {
3213 $file = $xml->addChild('file');
3214 $file->addAttribute('source-language', $record['languageoriginal']);
3215 $file->addAttribute('target-language', $record['languagetranslated']);
3216 $file->addAttribute('datatype', 'html');
3217 $file->addAttribute('original', $record['pagelink']);
3218
3219 $body = $file->addChild('body');
3220
3221 $translations = json_decode($record['translations'], true) ?: [];
3222 foreach ($translations as $source => $target) {
3223 $unit = $body->addChild('trans-unit');
3224 $unit->addAttribute('id', md5($source));
3225 $unit->addChild('source', htmlspecialchars($source));
3226 $unit->addChild('target', htmlspecialchars($target));
3227 }
3228 }
3229
3230 echo $xml->asXML();
3231 exit;
3232 });
3233
3234 // Handle Import XLIFF
3235 add_action('admin_post_gptranslate_import_translations_xliff', function () {
3236 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce')) {
3237 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3238 }
3239
3240 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
3241 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
3242 }
3243
3244 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3245 if (!is_uploaded_file($tmp_name)) {
3246 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3247 }
3248
3249 $xml = simplexml_load_file($tmp_name);
3250 if (!$xml) {
3251 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_XLIFF_FORMAT')));
3252 }
3253
3254 global $wpdb;
3255 $table = $wpdb->prefix . 'gptranslate';
3256
3257 foreach ($xml->file as $file) {
3258 $sourceLang = (string)$file['source-language'];
3259 $targetLang = (string)$file['target-language'];
3260 $pagelink = (string)$file['original'];
3261
3262 $translations = [];
3263 foreach ($file->body->{'trans-unit'} as $unit) {
3264 $src = (string)$unit->source;
3265 $tgt = (string)$unit->target;
3266 $translations[$src] = $tgt;
3267 }
3268
3269 $json_translations = wp_json_encode($translations);
3270
3271 $exists = $wpdb->get_var($wpdb->prepare(
3272 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
3273 $pagelink, $sourceLang, $targetLang
3274 )); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
3275
3276 if ($exists) {
3277 // �
3278 Update only the translations and date, keep everything else intact
3279 $wpdb->update(
3280 $table,
3281 [
3282 'translations' => $json_translations,
3283 'translate_date' => current_time('mysql'),
3284 ],
3285 ['id' => $exists]
3286 ); // phpcs:ignore
3287 } else {
3288 // �
3289 Insert full record only if it doesn't exist
3290 $data = [
3291 'pagelink' => $pagelink,
3292 'translated_alias' => '',
3293 'translations' => $json_translations,
3294 'alt_translations' => '[]',
3295 'languageoriginal' => $sourceLang,
3296 'languagetranslated'=> $targetLang,
3297 'published' => 1,
3298 'translate_date' => current_time('mysql'),
3299 'translation_engine'=> 'chatgpt',
3300 ];
3301 $wpdb->insert($table, $data); // phpcs:ignore
3302 }
3303 }
3304
3305 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
3306 exit;
3307 });
3308
3309 // Handle Export XML Sitemap
3310 add_action('admin_post_gptranslate_export_xml_sitemap', function () {
3311 if (!current_user_can('manage_options') || !isset($_POST['gptranslate_export_xml_sitemap']) || $_POST['gptranslate_export_xml_sitemap'] !== 'b62d18a19b') {
3312 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3313 }
3314
3315 global $wpdb;
3316 $table = $wpdb->prefix . 'gptranslate';
3317
3318 // Get language filter from POST parameter
3319 $language_filter = isset($_POST['sitemap_language']) ? sanitize_text_field(wp_unslash($_POST['sitemap_language'])) : '';
3320
3321 // Solo record con translated_alias non vuoto
3322 $query = "
3323 SELECT *
3324 FROM $table
3325 WHERE translated_alias IS NOT NULL
3326 AND translated_alias != ''";
3327
3328 // Add language filter if specified and not "all"
3329 if (!empty($language_filter) && $language_filter !== 'all') {
3330 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
3331 }
3332
3333 $query .= " ORDER BY translate_date DESC";
3334
3335 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3336
3337 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3338 $fileDate = date_i18n('Y-m-d', strtotime($localDate));
3339
3340 // Add language suffix to filename if specific language is selected
3341 if (!empty($language_filter) && $language_filter !== 'all') {
3342 $fileDate .= '-' . sanitize_file_name($language_filter);
3343 }
3344
3345 $dom = new DOMDocument('1.0', 'UTF-8');
3346 $dom->preserveWhiteSpace = false;
3347 $dom->formatOutput = true;
3348
3349 $urlset = $dom->createElement('urlset');
3350 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
3351 $dom->appendChild($urlset);
3352
3353 if (empty($records)) {
3354 $urlset->appendChild($dom->createTextNode(''));
3355 }
3356
3357 foreach ($records as $record) {
3358 $url = $dom->createElement('url');
3359 $loc = $dom->createElement('loc');
3360 $loc->appendChild($dom->createTextNode($record['translated_alias']));
3361 $url->appendChild($loc);
3362
3363 if (!empty($record['translate_date'])) {
3364 $lastmod = $dom->createElement('lastmod', date('c', strtotime($record['translate_date'])));
3365 $url->appendChild($lastmod);
3366 }
3367
3368 $urlset->appendChild($url);
3369 }
3370
3371 header('Content-Type: application/xml; charset=UTF-8');
3372 header('Content-Disposition: attachment; filename="gptranslate-sitemap-' . $fileDate . '.xml"');
3373 echo $dom->saveXML();
3374 exit;
3375 });
3376
3377 // Register Ajax handler
3378 add_action('wp_ajax_gptranslate_migrate_translations', function() {
3379 if (!current_user_can('manage_options')) {
3380 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3381 }
3382
3383 check_ajax_referer('gptranslate_migrate_translations');
3384
3385 global $wpdb;
3386 $table = $wpdb->prefix . 'gptranslate';
3387 $old = isset($_POST['old_domain']) ? sanitize_text_field(wp_unslash($_POST['old_domain'])) : '';
3388 $new = isset($_POST['new_domain']) ? sanitize_text_field(wp_unslash($_POST['new_domain'])) : '';
3389
3390 if (empty($old) || empty($new)) {
3391 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_MISSING_DOMAIN_VALUES')));
3392 }
3393
3394 $query = $wpdb->prepare(
3395 "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
3396 $old, $new, $old, $new
3397 );
3398
3399 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
3400 $result = $wpdb->query($query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3401
3402 if ($result === false) {
3403 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_DATABASE_ERROR')));
3404 } else {
3405 wp_send_json_success();
3406 }
3407 });
3408
3409 function gptranslate_export_settings() {
3410 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_export_settings', 'gptranslate_export_settings_nonce')) {
3411 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
3412 }
3413
3414 $options = get_option( 'gptranslate_options', [] );
3415
3416 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3417 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
3418
3419 header( 'Content-Type: application/json' );
3420 header( 'Content-Disposition: attachment; filename="gptranslate-settings-' . $fileDate . '.json"' );
3421 echo wp_json_encode( $options );
3422 exit;
3423 }
3424 add_action( 'admin_post_gptranslate_export_settings', 'gptranslate_export_settings' );
3425
3426 function gptranslate_import_settings() {
3427 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_import_settings', 'gptranslate_import_settings_nonce')) {
3428 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
3429 }
3430
3431 if ( empty( $_FILES['gptranslate_settings_file']['tmp_name'] ) ) {
3432 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')) );
3433 }
3434
3435 $content = file_get_contents( $_FILES['gptranslate_settings_file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3436 $decoded = json_decode( $content, true );
3437
3438 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
3439 wp_die( esc_html(GPTranslate::loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS') ) );
3440 }
3441
3442 // Optional: sanitize known values, or just update if you're confident of source
3443 update_option( 'gptranslate_options', $decoded );
3444
3445 wp_safe_redirect( admin_url( 'admin.php?page=gptranslate-settings&settingsimported=1' ) );
3446 exit;
3447 }
3448 add_action( 'admin_post_gptranslate_import_settings', 'gptranslate_import_settings' );
3449
3450 // ============================================================================
3451 // Shortcode [gptranslate] - Renders the language switcher at the shortcode position
3452 // ============================================================================
3453 add_shortcode('gptranslate', function ($atts) {
3454 $settings = get_option("gptranslate_options");
3455
3456 // Disable interface
3457 if (!empty($settings['disable_control'])) {
3458 return '';
3459 }
3460
3461 $wrapper_class = 'gptranslate_wrapper';
3462 $custom_selector = $settings['wrapper_selector'] ?? '.gptranslate_wrapper';
3463
3464 // If the user has set a custom wrapper selector (not the default), use that class
3465 if ($custom_selector !== '.gptranslate_wrapper' && strpos($custom_selector, '.') === 0) {
3466 $wrapper_class = substr($custom_selector, 1);
3467 }
3468
3469 // Flag that the shortcode was used, to prevent duplicate wrapper in footer
3470 if (!defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
3471 define('GPTRANSLATE_SHORTCODE_RENDERED', true);
3472 }
3473
3474 return '<div class="' . esc_attr($wrapper_class) . '"></div>';
3475 });
3476
3477 add_action('wp_footer', function () {
3478 // Add the default target container if default CSS selector
3479 $settings = get_option("gptranslate_options");
3480
3481 // Disable interface
3482 if($settings ['disable_control']) {
3483 $settings ['wrapper_selector'] = '';
3484 }
3485
3486 // Skip the automatic footer wrapper if shortcode was already used
3487 if (defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
3488 return;
3489 }
3490
3491 if ($settings ['wrapper_selector'] == '.gptranslate_wrapper') {
3492 echo '<div class="gptranslate_wrapper" id="gpt-wrapper"></div>';
3493 }
3494 });
3495
3496 // Get available (enabled) languages for sitemap filtering (excluding original language)
3497 function gptranslate_get_available_languages() {
3498 $opts = get_option('gptranslate_options', array());
3499
3500 // Get enabled target languages from settings
3501 $enabled_languages = isset($opts['languages']) ? array_map('strtolower', (array)$opts['languages']) : array();
3502
3503 // Exclude the original/source language
3504 $original_language = isset($opts['language']) ? strtolower($opts['language']) : '';
3505 $enabled_languages = array_filter($enabled_languages, function($lang) use ($original_language) {
3506 return $lang !== $original_language;
3507 });
3508
3509 return $enabled_languages ? array_values($enabled_languages) : array();
3510 }
3511
3512 // ============================================================================
3513 // AUTO-CRAWL ON POST PUBLISH
3514 // Automatically crawl new/updated posts when they are published
3515 // ============================================================================
3516
3517 /**
3518 * Hook triggered when a post is published/saved.
3519 * Saves the post URL to a transient queue if auto-crawl is enabled.
3520 */
3521 add_action('wp_after_insert_post', function($post_id, $post, $update, $post_before) {
3522 // Check if auto-crawl is enabled in settings
3523 $settings = get_option('gptranslate_options', []);
3524 // Handle both string '1' and integer 1 for the setting value
3525 $auto_crawl_enabled = isset($settings['auto_crawl_on_publish']) && ($settings['auto_crawl_on_publish'] == 1 || $settings['auto_crawl_on_publish'] === '1');
3526
3527 if (!$auto_crawl_enabled) {
3528 return;
3529 }
3530
3531 // Only trigger auto-crawl when post transitions TO published status (first publish only)
3532 $was_published = $post_before && $post_before->post_status === 'publish';
3533 $is_now_published = $post->post_status === 'publish';
3534
3535 // Must be newly published (not already published before)
3536 if (!($is_now_published && !$was_published)) {
3537 return;
3538 }
3539
3540 // Skip post types that shouldn't be translated (attachments, revisions, etc.)
3541 $exclude_types = ['attachment', 'revision', 'nav_menu_item'];
3542 if (in_array($post->post_type, $exclude_types, true)) {
3543 return;
3544 }
3545
3546 // Skip if this is an auto-draft
3547 if ($post->post_type === 'post' || $post->post_type === 'page') {
3548 // Get the post URL
3549 $post_url = get_permalink($post_id);
3550 if (empty($post_url)) {
3551 return;
3552 }
3553
3554 // Add language prefix to URL if needed (prevents 301 redirect that strips UTF-8 chars)
3555 $settings = get_option('gptranslate_options', []);
3556 if (!empty($settings['rewrite_language_url']) && empty($settings['omit_prefix_original_language'])) {
3557 $default_lang = $settings['language'] ?? 'en';
3558
3559 if (!empty($settings['subfolder_installation'])) {
3560 // Subfolder installation: add language prefix after subfolder
3561 // e.g., http://site.com/subfolder/page → http://site.com/subfolder/en/page
3562 $site_url = trailingslashit(get_site_url());
3563 $relative_path = str_replace($site_url, '', $post_url);
3564 $post_url = $site_url . $default_lang . '/' . ltrim($relative_path, '/');
3565 } else {
3566 // Root installation: add language prefix after domain
3567 // e.g., http://site.com/page → http://site.com/en/page
3568 $post_url = preg_replace(
3569 '#^(https?://[^/]+)/#',
3570 '$1/' . $default_lang . '/',
3571 $post_url
3572 );
3573 }
3574 }
3575
3576 // Save the URL to transient queue (per-user, 5 minute TTL)
3577 $queue_key = 'gpt_auto_crawl_queue_' . get_current_user_id();
3578 set_transient($queue_key, [
3579 'url' => $post_url,
3580 'post_id' => $post_id,
3581 'timestamp' => time()
3582 ], 5 * MINUTE_IN_SECONDS);
3583 }
3584 }, 10, 4);
3585
3586 /**
3587 * AJAX handler to clear the auto-crawl queue.
3588 * Called by auto-crawler.js after the post is published and user navigates to admin.
3589 * This prevents re-triggering the crawl on page reload.
3590 */
3591 add_action('wp_ajax_gpt_clear_auto_crawl_queue', function() {
3592 // Verify nonce for security
3593 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'gpt_auto_crawl_nonce')) {
3594 wp_send_json_error(['message' => 'Invalid nonce']);
3595 wp_die();
3596 }
3597
3598 // Delete the transient queue for current user
3599 $queue_key = 'gpt_auto_crawl_queue_' . get_current_user_id();
3600 delete_transient($queue_key);
3601
3602 wp_send_json_success();
3603 wp_die();
3604 });
3605
3606 // POST API REST Translations storage
3607 add_action('rest_api_init', function () {
3608 register_rest_route('gptranslate/v1', '/request', [
3609 'methods' => 'POST',
3610 'callback' => 'gpt_handle_request',
3611 'permission_callback' => 'gptranslate_public_permission'
3612 ]);
3613 });
3614
3615 // Real-time XML Sitemap endpoint - public, generates sitemap on-the-fly
3616 add_action('rest_api_init', function () {
3617 register_rest_route('gptranslate/v1', '/sitemap.xml', [
3618 'methods' => 'GET',
3619 'callback' => 'gptranslate_realtime_sitemap',
3620 'permission_callback' => '__return_true'
3621 ]);
3622 });
3623
3624 function gptranslate_realtime_sitemap() {
3625 global $wpdb;
3626 $table = $wpdb->prefix . 'gptranslate';
3627
3628 // Get language filter from query parameter
3629 $language_filter = isset($_GET['language']) ? sanitize_text_field(wp_unslash($_GET['language'])) : '';
3630
3631 $query = "
3632 SELECT translated_alias, translate_date
3633 FROM $table
3634 WHERE translated_alias IS NOT NULL
3635 AND translated_alias != ''";
3636
3637 // Add language filter if specified and not "all"
3638 if (!empty($language_filter) && $language_filter !== 'all') {
3639 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
3640 }
3641
3642 $query .= " ORDER BY translate_date DESC";
3643
3644 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3645
3646 $dom = new DOMDocument('1.0', 'UTF-8');
3647 $dom->preserveWhiteSpace = false;
3648 $dom->formatOutput = true;
3649
3650 $urlset = $dom->createElement('urlset');
3651 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
3652 $dom->appendChild($urlset);
3653
3654 if (empty($records)) {
3655 $urlset->appendChild($dom->createTextNode(''));
3656 }
3657
3658 foreach ($records as $record) {
3659 $url = $dom->createElement('url');
3660 $loc = $dom->createElement('loc');
3661 $loc->appendChild($dom->createTextNode($record['translated_alias']));
3662 $url->appendChild($loc);
3663
3664 if (!empty($record['translate_date'])) {
3665 $lastmod = $dom->createElement('lastmod', gmdate('c', strtotime($record['translate_date'])));
3666 $url->appendChild($lastmod);
3667 }
3668
3669 $urlset->appendChild($url);
3670 }
3671
3672 header('Content-Type: application/xml; charset=UTF-8');
3673 echo $dom->saveXML();
3674 exit;
3675 }
3676
3677 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
3678 // Remove the default 'Settings' item
3679 unset($links[0]);
3680
3681 $wizard_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-wizard')) . '">Wizard</a>';
3682 array_unshift($links, $wizard_link);
3683
3684 $settings_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-settings')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
3685 array_unshift($links, $settings_link);
3686
3687 $translations_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_TRANSLATIONS')) . '</a>';
3688 array_unshift($links, $translations_link);
3689
3690 return $links;
3691 });
3692
3693 /**
3694 * Get (or lazily create) the cryptographically random API secret.
3695 *
3696 * Replaces the predictable hash('sha256', get_site_url()) shared secret.
3697 * The value is generated once with wp_generate_password() (which uses
3698 * a cryptographically secure RNG) and persisted in wp_options.
3699 *
3700 * @return string
3701 */
3702 function gptranslate_get_api_secret() {
3703 $secret = get_option( 'gptranslate_api_secret' );
3704 if ( empty( $secret ) || ! is_string( $secret ) || strlen( $secret ) < 32 ) {
3705 $secret = wp_generate_password( 64, false, false );
3706 update_option( 'gptranslate_api_secret', $secret, false );
3707 }
3708 return $secret;
3709 }
3710
3711 /**
3712 * Permission callback public API
3713 * @param WP_REST_Request $request
3714 * @return bool|WP_Error
3715 */
3716 function gptranslate_public_permission( WP_REST_Request $request ) {
3717 // 1) Random API secret sent via header (constant-time comparison)
3718 $headerApiKey = (string) $request->get_header('x-gptranslate-key');
3719 $restApiKey = gptranslate_get_api_secret();
3720 if ( ! hash_equals( $restApiKey, $headerApiKey ) ) {
3721 return new WP_Error( 'rest_forbidden', esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FORBIDDEN_APIKEY')), [ 'status' => 403 ] );
3722 }
3723
3724 // 2) Nonce verification for write operations (CSRF protection)
3725 $params = $request->get_json_params();
3726 if ( empty( $params ) ) {
3727 $params = $request->get_body_params();
3728 }
3729 $task = isset( $params['task'] ) ? sanitize_text_field( $params['task'] ) : '';
3730 $writeTasks = array( 'storetranslations', 'syncTranslation' );
3731 if ( in_array( $task, $writeTasks, true ) ) {
3732 $nonce = $request->get_header('X-WP-Nonce');
3733 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
3734 return new WP_Error( 'rest_forbidden_nonce', 'Invalid or expired security token (nonce).', [ 'status' => 403 ] );
3735 }
3736 }
3737
3738 // 3) Admin-only tasks: require manage_options capability
3739 $adminTasks = array( 'syncTranslation' );
3740 if ( in_array( $task, $adminTasks, true ) && ! current_user_can( 'manage_options' ) ) {
3741 return new WP_Error( 'rest_forbidden_admin', 'Admin access required.', [ 'status' => 403 ] );
3742 }
3743
3744 return true;
3745 }
3746
3747 /**
3748 * Normalize WP URL
3749 * @param string $url
3750 * @return string
3751 */
3752 function gpt_trailingslashit_url($url) {
3753 $parsed = wp_parse_url($url);
3754 if (empty($parsed['path'])) {
3755 $parsed['path'] = '/';
3756 } else {
3757 $parsed['path'] = trailingslashit(untrailingslashit($parsed['path']));
3758 }
3759
3760 $rebuilt = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : '';
3761 $rebuilt .= $parsed['host'] ?? '';
3762 $rebuilt .= $parsed['path'];
3763 if (!empty($parsed['query'])) {
3764 $rebuilt .= '?' . $parsed['query'];
3765 }
3766 if (!empty($parsed['fragment'])) {
3767 $rebuilt .= '#' . $parsed['fragment'];
3768 }
3769
3770 return $rebuilt;
3771 }
3772
3773 /**
3774 * Callback per GET/STORE translations via REST.
3775 * Frontend API
3776 *
3777 * @param WP_REST_Request $request
3778 * @return WP_REST_Response
3779 */
3780 function gpt_handle_request( WP_REST_Request $request ) {
3781 global $wpdb;
3782
3783 $table = $wpdb->prefix . 'gptranslate';
3784
3785 $params = $request->get_json_params();
3786
3787 if(!$params) {
3788 $params = $request->get_body_params();
3789 }
3790
3791 // Sanitize input params
3792 $task = sanitize_text_field( $params['task'] ?? '' );
3793 $pageLink = esc_url_raw( $params['pagelink'] ?? '' );
3794 $translatedAlias = esc_url_raw( $params['translated_alias'] ?? '' );
3795 $languageOriginal = sanitize_text_field( $params['language_original'] ?? '' );
3796 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
3797 $translationEngine = sanitize_text_field( $params['translation_engine'] ?? '' );
3798 $retriggerTranslation = (int) sanitize_text_field( $params['retrigger'] ?? false );
3799 $isIncremental = (int) sanitize_text_field( $params['incremental'] ?? 0 );
3800
3801 $now = current_time( 'mysql' );
3802
3803 $response = [ 'result' => false ];
3804
3805 if ( $task === 'storetranslations' ) {
3806 // Ensure there is not a mismatching insert with the same languages
3807 if($languageOriginal == $languageTranslated) {
3808 $response['result'] = true;
3809 return rest_ensure_response( $response );
3810 }
3811
3812 // Fetch raw param (could be array or JSON string)
3813 $rawFull = $params['translations'] ?? '[]';
3814 $rawAlt = $params['alt_translations'] ?? '[]';
3815
3816 // If it’s already a string (JSON), use it directly.
3817 // If it’s an array (unlikely with FormData), JSON encode it.
3818 if ( is_string( $rawFull ) && json_decode( $rawFull ) !== null ) {
3819 $fullTranslations = $rawFull;
3820 } else {
3821 $fullTranslations = wp_json_encode( (array) $rawFull );
3822 }
3823
3824 if ( is_string( $rawAlt ) && json_decode( $rawAlt ) !== null ) {
3825 $altTranslations = $rawAlt;
3826 } else {
3827 $altTranslations = wp_json_encode( (array) $rawAlt );
3828 }
3829
3830 // Check if record already exists
3831 $existing = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3832 "SELECT id FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3833 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3834 "\n AND languageoriginal = %s" .
3835 "\n AND languagetranslated = %s",
3836 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3837 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
3838
3839 $existing_id = $existing ? (int)$existing : null;
3840 $opts = get_option('gptranslate_options', []);
3841
3842 // Only if it is a retrigger then ignore the db processing
3843 if($retriggerTranslation !== 1) {
3844 if ( $existing_id ) {
3845 if ( $isIncremental ) {
3846 // Incremental update: ALWAYS merge only new/modified translations (safe - never touch alt_translations or translated_alias)
3847 $existing_record = $wpdb->get_row( $wpdb->prepare(
3848 "SELECT translations FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
3849 " WHERE id = %d",
3850 (int) $existing_id
3851 ), ARRAY_A );
3852
3853 if ( $existing_record ) {
3854 // Merge translations: respect lock_translations setting
3855 $existing_trans = json_decode( $existing_record['translations'], true ) ?: [];
3856 $new_trans = json_decode( $fullTranslations, true ) ?: [];
3857
3858 // If lock_translations enabled: preserve existing (even more conservative on incremental)
3859 // If lock_translations disabled: allow updates to existing translations
3860 if ( !empty($opts['lock_translations']) ) {
3861 $merged_trans = array_merge( $new_trans, $existing_trans ); // existing wins
3862 } else {
3863 $merged_trans = array_merge( $existing_trans, $new_trans ); // new wins (updates)
3864 }
3865
3866 $update_data = [
3867 'translations' => wp_json_encode( $merged_trans ),
3868 'translate_date' => $now,
3869 'translation_engine' => $translationEngine,
3870 // Never touch alt_translations or translated_alias on incremental updates
3871 ];
3872
3873 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3874 $table,
3875 $update_data,
3876 [ 'id' => (int) $existing_id ],
3877 [ '%s', '%s', '%s' ],
3878 [ '%d' ]
3879 );
3880 $response['result'] = ( $updated !== false );
3881 } else {
3882 $response['result'] = false;
3883 }
3884 } else {
3885 // Full (non-incremental) update: protected by lock_translations setting
3886 if ( !empty($opts['lock_translations']) ) {
3887 // lock_translations enabled: skip full UPDATE
3888 $response['result'] = false;
3889 } else {
3890 // lock_translations disabled: overwrite everything (2.29 behavior)
3891 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3892 $table,
3893 [
3894 'translations' => $fullTranslations,
3895 'alt_translations' => $altTranslations,
3896 'translated_alias' => $translatedAlias,
3897 'translate_date' => $now,
3898 'translation_engine' => $translationEngine,
3899 ],
3900 [ 'id' => (int) $existing_id ],
3901 [ '%s', '%s', '%s', '%s', '%s' ],
3902 [ '%d' ]
3903 );
3904 $response['result'] = ( $updated !== false );
3905 }
3906 }
3907 } else {
3908 // INSERT
3909 $insert_data = [
3910 'pagelink' => $pageLink,
3911 'translations' => $fullTranslations,
3912 'alt_translations' => $altTranslations,
3913 'translated_alias' => $translatedAlias,
3914 'languageoriginal' => $languageOriginal,
3915 'languagetranslated' => $languageTranslated,
3916 'published' => 1,
3917 'translate_date' => $now,
3918 'translation_engine' => $translationEngine,
3919 ];
3920
3921 $inserted = $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3922 $table,
3923 $insert_data,
3924 [ '%s','%s','%s','%s','%s','%s','%d','%s','%s' ]
3925 );
3926 $response['result'] = ( $inserted !== false );
3927 }
3928 } else {
3929 $response['result'] = true;
3930 }
3931 } elseif ($task === 'gettranslations') {
3932 $opts = get_option('gptranslate_options', []);
3933 if (!empty($opts['realtime_translations']) || $retriggerTranslation === 1) {
3934 $response['result'] = false;
3935 } else {
3936 // Prepare decoded version for URL matching
3937 $pageLinkDecoded = urldecode($pageLink);
3938
3939 if ($opts['rewrite_language_url'] == 1 && $opts['rewrite_language_alias'] == 1) {
3940 // Check 8 variants (with/without slash + encoded/decoded)
3941 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3942 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3943 "\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)" .
3944 "\n AND languageoriginal = %s" .
3945 "\n AND languagetranslated = %s" .
3946 "\n AND published = 1",
3947 rtrim($pageLink, '/'),
3948 rtrim($pageLink, '/') . '/',
3949 rtrim($pageLinkDecoded, '/'),
3950 rtrim($pageLinkDecoded, '/') . '/',
3951 rtrim($pageLink, '/'),
3952 rtrim($pageLink, '/') . '/',
3953 rtrim($pageLinkDecoded, '/'),
3954 rtrim($pageLinkDecoded, '/') . '/',
3955 $languageOriginal,
3956 $languageTranslated
3957 ), ARRAY_A);
3958 } else {
3959 // Check 4 variants (with/without slash + encoded/decoded)
3960 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3961 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3962 "\n WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s)" .
3963 "\n AND languageoriginal = %s" .
3964 "\n AND languagetranslated = %s" .
3965 "\n AND published = 1",
3966 rtrim($pageLink, '/'),
3967 rtrim($pageLink, '/') . '/',
3968 rtrim($pageLinkDecoded, '/'),
3969 rtrim($pageLinkDecoded, '/') . '/',
3970 $languageOriginal,
3971 $languageTranslated
3972 ), ARRAY_A);
3973 }
3974
3975 if ($row) {
3976 $response['result'] = true;
3977 $response['translations'] = json_decode($row['translations'], true) ?: [];
3978 $response['alt_translations'] = json_decode($row['alt_translations'], true) ?: [];
3979 $response['translated_alias'] = $row['translated_alias'];
3980 $response['pagelink_alias'] = $row['pagelink'];
3981 } else {
3982 $response['result'] = false;
3983 }
3984 }
3985 } elseif ($task == 'getaliastranslation') {
3986 // Always perform a new realtime translation if the option is enabled
3987 try {
3988 $row = $wpdb->get_row( $wpdb->prepare(
3989 "SELECT translated_alias FROM {$table}" .
3990 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3991 "\n AND languageoriginal = %s" .
3992 "\n AND languagetranslated = %s" .
3993 "\n AND published = 1",
3994 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3995 ), ARRAY_A );
3996
3997 if ( $row ) {
3998 $response['result'] = true;
3999 $response['translated_alias'] = $row['translated_alias'] ?? '';
4000 } else {
4001 $response['result'] = false;
4002 }
4003 } catch ( Exception $e ) {
4004 $response['result'] = false;
4005 $response['exception'] = $e->getMessage();
4006 }
4007 } elseif ( $task === 'syncTranslation' ) {
4008 $original = wp_unslash( $params['original'] ?? '' );
4009 $translated = wp_unslash( $params['translated'] ?? '' );
4010 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
4011 $requestedType = sanitize_text_field( $params['translation_type'] ?? 'translations' );
4012 $translationType = in_array( $requestedType, array( 'translations', 'alt_translations' ), true ) ? $requestedType : 'translations';
4013
4014 // Recupera tutti i record nella lingua target
4015 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
4016 "SELECT id, {$translationType}, languagetranslated" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
4017 "\n FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
4018 "\n WHERE languagetranslated = %s",
4019 $languageTranslated
4020 ) );
4021
4022 $updatedCount = 0;
4023
4024 foreach ( $rows as $row ) {
4025 $currentTranslations = json_decode( $row->$translationType, true );
4026
4027 if ( is_array( $currentTranslations ) && array_key_exists( $original, $currentTranslations ) ) {
4028 // Aggiorna la traduzione e salva
4029 $currentTranslations[ $original ] = $translated;
4030 $jsonUpdated = wp_json_encode( $currentTranslations );
4031
4032 $success = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
4033 $table,
4034 [ $translationType => $jsonUpdated, 'translate_date' => $now ],
4035 [ 'id' => $row->id ],
4036 [ '%s', '%s' ],
4037 [ '%d' ]
4038 );
4039
4040 if ( $success !== false ) {
4041 $updatedCount++;
4042 }
4043 }
4044 }
4045
4046 $response['result'] = $updatedCount > 0;
4047 } elseif ($task == 'gettranslatedaliases') {
4048 try {
4049 if ($languageTranslated) {
4050 $rows = $wpdb->get_results(
4051 $wpdb->prepare(
4052 "SELECT pagelink, translated_alias
4053 FROM {$table}
4054 WHERE languagetranslated = %s
4055 AND published = 1",
4056 $languageTranslated
4057 ),
4058 ARRAY_A
4059 );
4060 } elseif ($languageOriginal) {
4061 $rows = $wpdb->get_results(
4062 $wpdb->prepare(
4063 "SELECT translated_alias AS pagelink, pagelink AS translated_alias
4064 FROM {$table}
4065 WHERE languageoriginal = %s
4066 AND published = 1",
4067 $languageOriginal
4068 ),
4069 ARRAY_A
4070 );
4071 } else {
4072 $response['result'] = false;
4073 echo wp_json_encode($response);
4074 exit;
4075 }
4076
4077 if ($rows) {
4078 $encodedResult = [];
4079
4080 foreach ($rows as $row) {
4081
4082 // Normalizza WordPress-style (CON trailing slash)
4083 $pagelink = gpt_trailingslashit_url($row['pagelink']);
4084 $translatedAlias = !empty($row['translated_alias']) ? gpt_trailingslashit_url($row['translated_alias']) : '';
4085
4086 // Encode pagelink (solo path)
4087 $parsedUrl = wp_parse_url($pagelink);
4088 $encodedPagelink = $pagelink;
4089
4090 if (!empty($parsedUrl['path'])) {
4091 $pathParts = explode('/', $parsedUrl['path']);
4092 $encodedParts = array_map('rawurlencode', $pathParts);
4093 $encodedPath = implode('/', $encodedParts);
4094
4095 $encodedPagelink = ($parsedUrl['scheme'] ?? '') . '://' . ($parsedUrl['host'] ?? '');
4096 $encodedPagelink .= $encodedPath;
4097 if (!empty($parsedUrl['query'])) {
4098 $encodedPagelink .= '?' . $parsedUrl['query'];
4099 }
4100 if (!empty($parsedUrl['fragment'])) {
4101 $encodedPagelink .= '#' . $parsedUrl['fragment'];
4102 }
4103 }
4104
4105 // Encode translated alias
4106 $encodedAlias = $translatedAlias;
4107 if (!empty($translatedAlias)) {
4108 $parsedAlias = wp_parse_url($translatedAlias);
4109 if (!empty($parsedAlias['path'])) {
4110 $pathParts = explode('/', $parsedAlias['path']);
4111 $encodedParts = array_map('rawurlencode', $pathParts);
4112 $encodedPath = implode('/', $encodedParts);
4113
4114 $encodedAlias = ($parsedAlias['scheme'] ?? '') . '://' . ($parsedAlias['host'] ?? '');
4115 $encodedAlias .= $encodedPath;
4116 if (!empty($parsedAlias['query'])) {
4117 $encodedAlias .= '?' . $parsedAlias['query'];
4118 }
4119 if (!empty($parsedAlias['fragment'])) {
4120 $encodedAlias .= '#' . $parsedAlias['fragment'];
4121 }
4122 }
4123 }
4124
4125 $encodedResult[$encodedPagelink] = [
4126 'pagelink' => $encodedPagelink,
4127 'translated_alias' => $encodedAlias
4128 ];
4129 }
4130
4131 $response['result'] = true;
4132 $response['translated_aliases'] = $encodedResult;
4133
4134 } else {
4135 $response['result'] = false;
4136 }
4137
4138 } catch (Exception $e) {
4139 $response['result'] = false;
4140 $response['exception'] = $e->getMessage();
4141 }
4142 } elseif ( $task === 'deepseektranslations' ) {
4143 try {
4144 // Read raw JSON payload sent from JS
4145 $rawInput = file_get_contents( 'php://input' );
4146 $requestData = json_decode( $rawInput, true );
4147
4148 if ( ! $requestData || empty( $requestData['messages'] ) ) {
4149 throw new Exception( 'Invalid DeepSeek request payload' );
4150 }
4151
4152 // Get plugin options
4153 $opts = get_option( 'gptranslate_options', [] );
4154
4155 $deepseekApiKey = $opts['chatgpt_apikey'] ?? '';
4156 $deepseekModel = $opts['chatgpt_model'] ?? 'deepseek-chat';
4157
4158 if ( empty( $deepseekApiKey ) ) {
4159 throw new Exception( 'DeepSeek API key not configured' );
4160 }
4161
4162 // Fixed DeepSeek parameters (server controlled)
4163 $payload = [
4164 'model' => $deepseekModel,
4165 'messages' => $requestData['messages'],
4166 'max_tokens' => 4096,
4167 'temperature' => 0.5,
4168 ];
4169
4170 // Call DeepSeek API
4171 $ch = curl_init( 'https://api.deepseek.com/v1/chat/completions' );
4172 curl_setopt_array( $ch, [
4173 CURLOPT_POST => true,
4174 CURLOPT_RETURNTRANSFER => true,
4175 CURLOPT_HTTPHEADER => [
4176 'Content-Type: application/json',
4177 'Authorization: Bearer ' . $deepseekApiKey,
4178 ],
4179 CURLOPT_POSTFIELDS => wp_json_encode( $payload ),
4180 CURLOPT_TIMEOUT => 60,
4181 ] );
4182
4183 $apiResponse = curl_exec( $ch );
4184
4185 if ( $apiResponse === false ) {
4186 throw new Exception( 'DeepSeek cURL error: ' . curl_error( $ch ) );
4187 }
4188
4189 $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
4190 curl_close( $ch );
4191
4192 if ( $httpCode >= 400 ) {
4193 throw new Exception( 'DeepSeek API HTTP error: ' . $httpCode );
4194 }
4195
4196 // IMPORTANT:
4197 // Return DeepSeek response EXACTLY as the API returns it
4198 header( 'Content-Type: application/json; charset=utf-8' );
4199 echo $apiResponse;
4200 exit;
4201
4202 } catch ( Exception $e ) {
4203 // FALLBACK SAFE RESPONSE (OpenAI-compatible)
4204 header('Content-Type: application/json; charset=utf-8');
4205 echo wp_json_encode([
4206 'choices' => [[
4207 'finish_reason' => 'error',
4208 'message' => [
4209 'role' => 'assistant',
4210 'content' => ''
4211 ]
4212 ]]
4213 ]);
4214 exit;
4215 }
4216 } elseif ($task === 'deepltranslations') {
4217 try {
4218 // Use $params already parsed by WordPress REST API (php://input is consumed)
4219 if (empty ( $params ['texts'] )) {
4220 throw new Exception ( 'Invalid DeepL request payload' );
4221 }
4222
4223 // Get plugin options
4224 $opts = get_option ( 'gptranslate_options', [ ] );
4225
4226 $deeplApiKey = $opts ['chatgpt_apikey'] ?? '';
4227
4228 if (empty ( $deeplApiKey )) {
4229 throw new Exception ( 'DeepL API key not configured' );
4230 }
4231
4232 $sourceLanguage = sanitize_text_field ( $params ['source_lang'] ?? 'auto');
4233 $targetLanguage = sanitize_text_field ( $params ['target_lang'] ?? 'EN');
4234
4235 // Build query string manually - DeepL requires repeated "text" params (not text[0], text[1])
4236 $queryParts = [ ];
4237 $queryParts [] = 'source_lang=' . urlencode ( strtoupper ( $sourceLanguage ) );
4238 $queryParts [] = 'target_lang=' . urlencode ( strtoupper ( $targetLanguage ) );
4239
4240 foreach ( $params ['texts'] as $text ) {
4241 $queryParts [] = 'text=' . urlencode ( $text );
4242 }
4243
4244 $queryString = implode ( '&', $queryParts );
4245
4246 // Call DeepL API - Auto-detect endpoint based on API key type
4247 // Free API keys end with ':fx', paid keys don't have this suffix
4248 $deeplEndpoint = (strpos ( $deeplApiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
4249 $ch = curl_init ( $deeplEndpoint . '/v2/translate' );
4250 curl_setopt_array ( $ch, [
4251 CURLOPT_POST => true,
4252 CURLOPT_RETURNTRANSFER => true,
4253 CURLOPT_HTTPHEADER => [
4254 'Authorization: DeepL-Auth-Key ' . $deeplApiKey,
4255 'Content-Type: application/x-www-form-urlencoded'
4256 ],
4257 CURLOPT_POSTFIELDS => $queryString,
4258 CURLOPT_TIMEOUT => 60
4259 ] );
4260
4261 $apiResponse = curl_exec ( $ch );
4262
4263 if ($apiResponse === false) {
4264 throw new Exception ( 'DeepL cURL error: ' . curl_error ( $ch ) );
4265 }
4266
4267 $httpCode = curl_getinfo ( $ch, CURLINFO_HTTP_CODE );
4268 curl_close ( $ch );
4269
4270 if ($httpCode >= 400) {
4271 throw new Exception ( 'DeepL API HTTP error: ' . $httpCode . ' - ' . $apiResponse );
4272 }
4273
4274 // Return DeepL response EXACTLY as the API returns it
4275 header ( 'Content-Type: application/json; charset=utf-8' );
4276 echo $apiResponse;
4277 exit ();
4278 } catch ( Exception $e ) {
4279 wp_send_json_error ( [
4280 'message' => $e->getMessage ()
4281 ] );
4282 }
4283 }
4284
4285 return rest_ensure_response( $response );
4286 }
4287
4288 // Instantiate and run the app
4289 GPTranslate::get_instance();
4290
4291 // Optimizer compatibility – Phase 1 (early).
4292 // PHP-side exclusion filters must be registered at plugin load time so they
4293 // are in place before any optimizer (e.g. WP Rocket) reads them during its
4294 // own initialisation. Never runs in wp-admin (handled inside init_early).
4295 GPTranslate_Optimizer_Compat::init_early();
4296
4297 // Optimizer compatibility – Phase 2 (late).
4298 // HTML tag-manipulation filters (script_loader_tag, wp_inline_script_tag)
4299 // are registered here, after GPTranslate has decided whether to enqueue
4300 // scripts on this page. Bails immediately if gptranslate-main is not queued.
4301 add_action( 'wp_enqueue_scripts', [ 'GPTranslate_Optimizer_Compat', 'init_late' ], 999 );