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