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