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 21 hours ago flags 1 week ago includes 1 week ago language 21 hours ago ajax-handler.php 1 week ago gptranslate.php 21 hours 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
4284 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_NO_SWITCHER_DETECTED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_NO_SWITCHER_DETECTED')) . '";
1343 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
1344 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
1345 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
1346 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
1347 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
1348 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
1349 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
1350 const PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION')) . '";
1351 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL')) . '";
1352 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL')) . '";
1353 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES')) . '";
1354 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED')) . '";
1355 const PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED')) . '";
1356 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION')) . '";
1357 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER = "' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER')) . '";
1358 var gptranslateBaseCrawlerHome = "' . esc_js( trailingslashit( get_site_url() ) ) . '";
1359 var gptranslateSitemapUrl = "' . esc_js( rest_url( 'gptranslate/v1/sitemap.xml' ) ) . '";
1360 var gptranslateDefaultLanguage = "' . $opts['language'] . '";
1361 var gptranslateCrawlerTimeout = "' . (isset($opts['crawler_timeout']) ? $opts['crawler_timeout'] : '30') . '";
1362 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($opts['crawler_exclusions']) ? $opts['crawler_exclusions'] : ''))), ',')) . '";
1363 var gptranslateCrawlerSkipTranslated = ' . (int)($opts['crawler_skip_translated'] ?? 0) . ';
1364 var gptSubfolderInstallation = ' . (int)($opts['subfolder_installation'] ?? 0) . ';
1365 var gptranslateRewriteLanguageUrl = ' . (int)$opts['rewrite_language_url'] . ';
1366 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($opts['omit_prefix_original_language']) ? (int)$opts['omit_prefix_original_language'] : 0) . ';
1367 var gptVersionNumeric = ' . 0 . ';
1368 var gptranslateSitemapAvailableLanguages = ' . wp_json_encode(gptranslate_get_available_languages()) . ';
1369 </script>';
1370
1371
1372 echo '<table class="widefat fixed striped">';
1373 echo '<thead><tr>';
1374 echo '<th style="width: 1%"><input class="form-check-input" autocomplete="off" type="checkbox" id="checkall"></th>';
1375 echo '<th style="width: 2%;;white-space:nowrap">ID</th>';
1376 if($opts['rewrite_language_alias'] == 1) {
1377 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
1378 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATED_ALIAS')) . '</th>';
1379 echo '<th style="width: 18%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
1380 } else {
1381 echo '<th style="width: 25%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LINK_PAGE')) . '</th>';
1382 echo '<th style="width: 20%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL_TRANSLATED')) . '</th>';
1383 }
1384 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_ORIGINAL')) . '</th>';
1385 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_TRANSLATED')) . '</th>';
1386 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_PUBLISHED_GENERIC')) . '</th>';
1387 echo '<th style="width:5%">' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_ENGINE')) . '</th>';
1388 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_TRANSLATIONS_DATE')) . '</th>';
1389 echo '<th>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_ACTIONS')) . '</th>';
1390 echo '</tr></thead>';
1391
1392 echo '<tbody>';
1393
1394 $current_page = isset($_GET['paged']) && is_numeric($_GET['paged']) ? (int)$_GET['paged'] : 1;
1395 $offset = ($current_page - 1) * $records_per_page;
1396 $sql_count = "SELECT COUNT(*) FROM {$this->table_name} WHERE 1=1";
1397
1398 // Add dynamic filters
1399 if (!empty($searchFilter)) {
1400 if ($exactMatchFilter) {
1401 // Ricerca esatta
1402 $sql_count .= " AND (pagelink = '" . esc_sql($searchFilter) . "' OR translated_alias = '" . esc_sql($searchFilter) . "')";
1403 } else {
1404 // Ricerca LIKE (comportamento attuale)
1405 $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) . "%'" . ")";
1406 }
1407 }
1408 if ($publishedFilter !== '') {
1409 $sql_count .= " AND published = '" . esc_sql($publishedFilter) . "'";
1410 }
1411 if (!empty($languageFilter)) {
1412 $sql_count .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
1413 }
1414 if (!empty($engineFilter)) {
1415 $sql_count .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
1416 }
1417 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1418 $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
1419 $total_pages = ceil($total_records / $records_per_page);
1420
1421 // Load records count with filtering
1422 $sql_data = "SELECT * FROM {$this->table_name} WHERE 1=1";
1423
1424 // Add dynamic filters
1425 if (!empty($searchFilter)) {
1426 if ($exactMatchFilter) {
1427 // Ricerca esatta
1428 $sql_data .= " AND (pagelink = '" . esc_sql($searchFilter) . "' OR translated_alias = '" . esc_sql($searchFilter) . "')";
1429 } else {
1430 // Ricerca LIKE (comportamento attuale)
1431 $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) . "%'" . ")";
1432 }
1433 }
1434 if ($publishedFilter !== '') {
1435 $sql_data .= " AND published = '" . esc_sql($publishedFilter) . "'";
1436 }
1437 if (!empty($languageFilter)) {
1438 $sql_data .= " AND languagetranslated = '" . esc_sql($languageFilter) . "'";
1439 }
1440 if (!empty($engineFilter)) {
1441 $sql_data .= " AND translation_engine = '" . esc_sql($engineFilter) . "'";
1442 }
1443 $sql_data .= " ORDER BY translate_date DESC LIMIT $records_per_page OFFSET $offset";
1444
1445 // Load records with filtering and pagination
1446 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
1447 $records = $wpdb->get_results($sql_data); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1448
1449 // Message for no translations
1450 if (!count($records) && stripos($sql_data, "AND") === false) {
1451 echo '<div class="notice notice-success is-dismissible" id="notranslations-notice">';
1452 echo '<p>' . esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO_TRANSLATIONS')) . '</p>';
1453 echo '</div>';
1454 ?>
1455 <div id="gptranslate-zero-state" class="gptranslate-zero-state">
1456 <div class="gptranslate-zero-icon">
1457 <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">
1458 <g id="language">
1459 <g>
1460 <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
1461 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
1462 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
1463 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
1464 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
1465 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"/>
1466 </g>
1467 </g>
1468 </svg>
1469 </div>
1470
1471 <div class="gptranslate-zero-title">
1472 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_TITLE')); ?>
1473 </div>
1474
1475 <p class="gptranslate-zero-desc">
1476 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_DESC')); ?>
1477 </p>
1478
1479 <button id="gptranslate-start-crawler" class="button button-primary button-hero">
1480 <span class="dashicons-before dashicons-translation" aria-hidden="true"></span>
1481 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_BUTTON')); ?>
1482 </button>
1483
1484 <p class="gptranslate-zero-hint">
1485 <?php echo esc_html($this->loadTranslations('PLG_GPTRANSLATE_ZERO_HINT')); ?>
1486 </p>
1487 </div>
1488 <?php
1489 }
1490
1491 $urlPrefixReverseMap = $this->getCustomUrlPrefixReverseMap();
1492 foreach ( $records as $r ) {
1493 // Build a short summary of the translations
1494 $tr = json_decode( $r->translations, true );
1495 if ( is_array( $tr ) ) {
1496 $pairs = array_map(
1497 function( $k, $v ) {
1498 return esc_html( $k ) . '' . esc_html( $v );
1499 },
1500 array_keys( $tr ),
1501 array_values( $tr )
1502 );
1503 $short = implode( ', ', $pairs );
1504 $short = mb_substr( $short, 0, 80 ) . ( mb_strlen( $short ) > 80 ? '' : '' );
1505 } else {
1506 $short = '';
1507 }
1508
1509 // Escape all output
1510 $id = (int) $r->id;
1511 $link = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=edit&edit={$id}" ), 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
1512 $deleteLink = wp_nonce_url( admin_url( "admin.php?page=gptranslate&action=delete_translation&translation_id={$id}" ), 'gptranslate_delete_' . $id, '_gptranslate_nonce' );
1513 $origLang = esc_html( strtoupper( $r->languageoriginal ) );
1514 $transLang = esc_html( strtoupper( $r->languagetranslated ) );
1515 $pub = $r->published ? esc_html($this->loadTranslations('PLG_GPTRANSLATE_YES')) : esc_html($this->loadTranslations('PLG_GPTRANSLATE_NO'));
1516 $engine = esc_html( $r->translation_engine );
1517 $date = esc_html( $r->translate_date );
1518
1519 $langOriginal = esc_attr($r->languageoriginal);
1520
1521 // Path relativo o assoluto all'immagine della bandiera
1522 $flagUrlOriginal = plugins_url('flags/svg/' . $r->languageoriginal . '.svg', __FILE__);
1523 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1524 $flagUrlTranslated = plugins_url('flags/svg/' . $r->languagetranslated . '.svg', __FILE__);
1525 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1526 if (!empty($urlPrefixReverseMap)) {
1527 if (isset($urlPrefixReverseMap[$r->languageoriginal])) {
1528 $flagUrlOriginal = plugins_url('flags/svg/' . $urlPrefixReverseMap[$r->languageoriginal] . '.svg', __FILE__);
1529 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1530 }
1531 if (isset($urlPrefixReverseMap[$r->languagetranslated])) {
1532 $flagUrlTranslated = plugins_url('flags/svg/' . $urlPrefixReverseMap[$r->languagetranslated] . '.svg', __FILE__);
1533 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1534 }
1535 }
1536
1537 // Alternative flags check for list view
1538 $altFlagsOpts = isset($opts['alt_flags']) && is_array($opts['alt_flags']) ? $opts['alt_flags'] : [];
1539 $altFlagMap = [
1540 'en' => ['usa' => 'en-us', 'canada' => 'en-ca', 'ireland' => 'en-ie'],
1541 'pt' => ['brazil' => 'pt-br'],
1542 'es' => ['mexico' => 'es-mx', 'argentina' => 'es-ar', 'colombia' => 'es-co'],
1543 'fr' => ['quebec' => 'fr-qc'],
1544 'zh' => ['taiwan' => 'zh-TW'],
1545 'zt' => ['hongkong' => 'zh-HK'],
1546 'de' => ['austria' => 'de-at'],
1547 ];
1548 foreach ($altFlagMap as $langCode => $variants) {
1549 foreach ($variants as $country => $flagFile) {
1550 if (in_array($country, $altFlagsOpts)) {
1551 if ($r->languageoriginal === $langCode) {
1552 $flagUrlOriginal = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1553 $flagOriginal = '<img src="' . esc_url($flagUrlOriginal) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1554 }
1555 if ($r->languagetranslated === $langCode) {
1556 $flagUrlTranslated = plugins_url('flags/svg/' . $flagFile . '.svg', __FILE__);
1557 $flagTranslated = '<img src="' . esc_url($flagUrlTranslated) . '" alt="flag" style="width: 16px; vertical-align:middle; margin-right:4px;">'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
1558 }
1559 break;
1560 }
1561 }
1562 }
1563
1564 $togglePublishedUrl = wp_nonce_url(
1565 admin_url("admin.php?page=gptranslate&action=toggle_published&translation_id={$id}"),
1566 'gptranslate_toggle_' . $id,
1567 '_gptranslate_nonce'
1568 );
1569
1570 $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
1571 : '<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
1572
1573
1574 $pub = $r->published ? "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-published' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_UNPUBLISH')) . "'>" . $pubIcon . "</a>"
1575 : "<a href='{$togglePublishedUrl}' class='gpt-toggle gpt-unpublished' title='" . esc_attr($this->loadTranslations('PLG_GPTRANSLATE_PUBLISH')) . "'>" . $pubIcon . "</a>";
1576
1577 $local_date = get_date_from_gmt($date);
1578
1579 echo '<tr>';
1580 echo "<td style='width: 1%'><input class='form-check-input' autocomplete='off' type='checkbox' id='cb0' name='gptid[]' value='" . esc_attr($r->id) . "'></td>";
1581 echo "<td style='width: 2%;white-space:nowrap'>". esc_html($r->id) . "</td>";
1582 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>";
1583 if($opts['rewrite_language_alias'] == 1) {
1584 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>';
1585 }
1586 echo "<td>" . esc_html($short) . "</td>";
1587 echo "<td>" . wp_kses_post($flagOriginal) . " " . esc_html($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . strtoupper($langOriginal))) . "</td>";
1588 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>";
1589 echo "<td>" . wp_kses_post($pub) . "</td>";
1590 echo "<td><span class='gpt-label'>" . esc_html($this->loadTranslations('PLG_GPTRANSLATE_CHATGPT_TRANSLATION_ENGINE_' . strtoupper($engine) . '_ENGINE')) . "</span></td>";
1591 echo "<td>" . esc_html( date_i18n('l, d F Y \a\t H:i', strtotime($local_date)) ) . "</td>";
1592 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>";
1593 echo '</tr>';
1594 }
1595 echo '</tbody>';
1596 echo '</table>';
1597
1598 if ($total_pages > 1) {
1599 echo '<div class="tablenav"><div class="tablenav-pages">';
1600 for ($i = 1; $i <= $total_pages; $i++) {
1601 $url = add_query_arg(array_merge($_GET, ['paged' => $i]), admin_url('admin.php'));
1602 $class = ($i == $current_page) ? "class='current-page button'" : "class='button'";
1603 echo "<a " . wp_kses_post($class) . " href='" . esc_url($url) . "'>" . esc_html($i) . "</a> ";
1604 }
1605 echo '</div></div>';
1606 }
1607 }
1608
1609 echo '</div>';
1610 echo '</div>';
1611 }
1612
1613 /**
1614 * Load language file and translations
1615 * @return array
1616 */
1617 public function loadTranslations($key) {
1618 // Text translations
1619 static $adminLanguageStrings = null;
1620
1621 if($adminLanguageStrings === null) {
1622 // Use GPTRANSLATE_CURRENT_LANG if defined (server-side language), fallback to blog language
1623 $langTag = defined('GPTRANSLATE_CURRENT_LANG') ? GPTRANSLATE_CURRENT_LANG : get_bloginfo('language');
1624
1625 // Normalize language tag: "it" -> "it-IT"
1626 if(strlen($langTag) == 2) {
1627 $langTag = $langTag . '-' . strtoupper($langTag);
1628 }
1629
1630 // Try to load the specific language file
1631 $adminLanguageFile = dirname(__FILE__) . "/language/" . $langTag . "/gptranslate.ini";
1632 if(!file_exists($adminLanguageFile)) {
1633 // Fallback to English if specific language file doesn't exist
1634 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
1635 }
1636
1637 $adminLanguageStrings = array();
1638 if(file_exists($adminLanguageFile)) {
1639 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
1640 }
1641 }
1642
1643 if(array_key_exists($key, $adminLanguageStrings)) {
1644 return $adminLanguageStrings[$key];
1645 }
1646
1647 return $key;
1648 }
1649
1650 /**
1651 * Load language file and translations
1652 * @return array
1653 */
1654 public static function loadTranslation($key) {
1655 // Text translations
1656 static $adminLanguageStrings = null;
1657
1658 if($adminLanguageStrings === null) {
1659 // Use GPTRANSLATE_CURRENT_LANG if defined (server-side language), fallback to blog language
1660 $langTag = defined('GPTRANSLATE_CURRENT_LANG') ? GPTRANSLATE_CURRENT_LANG : get_bloginfo('language');
1661
1662 // Normalize language tag: "it" -> "it-IT"
1663 if(strlen($langTag) == 2) {
1664 $langTag = $langTag . '-' . strtoupper($langTag);
1665 }
1666
1667 // Try to load the specific language file
1668 $adminLanguageFile = dirname(__FILE__) . "/language/" . $langTag . "/gptranslate.ini";
1669 if(!file_exists($adminLanguageFile)) {
1670 // Fallback to English if specific language file doesn't exist
1671 $adminLanguageFile = dirname(__FILE__) . "/language/en-GB/gptranslate.ini";
1672 }
1673
1674 $adminLanguageStrings = array();
1675 if(file_exists($adminLanguageFile)) {
1676 $adminLanguageStrings = parse_ini_file($adminLanguageFile, false, INI_SCANNER_NORMAL);
1677 }
1678 }
1679
1680 if(array_key_exists($key, $adminLanguageStrings)) {
1681 return $adminLanguageStrings[$key];
1682 }
1683
1684 return $key;
1685 }
1686
1687 /**
1688 * Save translation record
1689 *
1690 * @access public
1691 */
1692 public function save_record() {
1693 global $wpdb;
1694
1695 // Retrieve and sanitize basic inputs
1696 $id = isset ( $_POST ['id'] ) ? intval ( $_POST ['id'] ) : 0;
1697 $formAction = isset ( $_POST ['action'] ) ? sanitize_key ( $_POST ['action'] ) : '';
1698
1699 if ( !isset($_POST['_gptranslate_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_gptranslate_nonce'])), 'gptranslate_save_record_action') ) {
1700 wp_die(esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'gptranslate');
1701 }
1702
1703 // Handle cancel action
1704 if ($formAction === 'cancel_gptranslate_record') {
1705 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
1706 exit ();
1707 }
1708
1709 // Sanitize pagelink
1710 $pagelink = isset ( $_POST ['pagelink'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['pagelink'] ) ) : '';
1711
1712 // Sanitize translated_alias
1713 $translatedAlias = isset ( $_POST ['translated_alias'] ) ? sanitize_text_field ( wp_unslash ( $_POST ['translated_alias'] ) ) : '';
1714
1715 // Process and sanitize translations JSON
1716 $raw_translations = filter_input( INPUT_POST, 'translations_json', FILTER_UNSAFE_RAW );
1717 $raw_translations = is_string($raw_translations) ? $raw_translations : '[]';
1718
1719 $decoded_translations = json_decode ( $raw_translations, true );
1720 if (! is_array ( $decoded_translations )) {
1721 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
1722 'response' => 400
1723 ] );
1724 }
1725 $clean_translations = $decoded_translations;
1726 $sanitized_translations_json = wp_json_encode ( $clean_translations );
1727
1728 // Process and sanitize alternative translations JSON
1729 $raw_alt = filter_input( INPUT_POST, 'alt_translations_json', FILTER_UNSAFE_RAW );
1730 $raw_alt = is_string($raw_translations) ? $raw_alt : '[]';
1731
1732 $decoded_alt = json_decode ( $raw_alt, true );
1733 if (! is_array ( $decoded_alt )) {
1734 wp_die ( esc_html($this->loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_ALTTRANSLATIONS')), esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_ERROR')), [
1735 'response' => 400
1736 ] );
1737 }
1738 $clean_alt = $decoded_alt;
1739 $sanitized_alt_json = wp_json_encode ( $clean_alt );
1740
1741 // Update database record
1742 $wpdb->update ( $this->table_name, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1743 'pagelink' => $pagelink,
1744 'translated_alias' => $translatedAlias,
1745 'translations' => $sanitized_translations_json,
1746 'alt_translations' => $sanitized_alt_json
1747 ], [
1748 'id' => $id
1749 ], [
1750 '%s',
1751 '%s',
1752 '%s'
1753 ], [
1754 '%d'
1755 ] );
1756
1757 // Redirect based on action
1758 if ($formAction === 'save_gptranslate_record_and_close') {
1759 wp_redirect ( admin_url ( 'admin.php?page=gptranslate' ) );
1760 } else {
1761 $url = admin_url( 'admin.php?page=gptranslate&action=edit&edit=' . $id );
1762 $url = wp_nonce_url( $url, 'gptranslate_edit_' . $id, '_gptranslate_nonce' );
1763 wp_redirect( html_entity_decode( $url ) );
1764 }
1765 exit ();
1766 }
1767
1768 public function gptranslate_handle_deletion() {
1769 // 1) Verify nonce
1770 $id = isset( $_GET['translation_id'] ) ? intval( $_GET['translation_id'] ) : 0;
1771 $nonce = isset( $_GET['_gptranslate_nonce'] ) ? wp_unslash( $_GET['_gptranslate_nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1772
1773 if ( ! wp_verify_nonce( $nonce, 'gptranslate_delete_' . $id ) ) {
1774 wp_die( esc_html($this->loadTranslations('PLG_GPTRANSLATE_GENERIC_SECURITY_ERROR')), 'Error', [ 'response' => 403 ] );
1775 }
1776
1777 // 2) Delete the row
1778 global $wpdb;
1779 $table = $wpdb->prefix . 'gptranslate';
1780 $deleted = $wpdb->delete( $table, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1781
1782 // 3) Redirect back with a message
1783 $redirect_url = add_query_arg(
1784 [
1785 'page' => 'gptranslate',
1786 'deleted' => $deleted ? '1' : '0',
1787 ],
1788 admin_url( 'admin.php' )
1789 );
1790 wp_redirect( $redirect_url );
1791 exit;
1792 }
1793
1794
1795 /**
1796 * Add main app frontend script
1797 *
1798 * @access public
1799 */
1800 public function enqueue_frontend_scripts() {
1801 add_filter('script_loader_tag', function($tag, $handle) {
1802 if ($handle === 'gptranslate-responsivevoice') {
1803 return str_replace('<script ', '<script defer ', $tag);
1804 }
1805
1806 if ($handle === 'gptranslate-jsonrepair') {
1807 return str_replace('<script ', '<script type="module" ', $tag);
1808 }
1809
1810 if ($handle === 'gptranslate-bstoast') {
1811 return str_replace('<script ', '<script type="module" ', $tag);
1812 }
1813 if ($handle === 'gptranslate-main') {
1814 // 1) prendi i valori raw
1815 $raw_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
1816 $raw_host = isset($_SERVER['HTTP_HOST']) ? wp_unslash($_SERVER['HTTP_HOST']) : '';
1817
1818 // 2) unslash WP
1819 $unslashed_uri = wp_unslash ( $raw_uri );
1820 $unslashed_host = wp_unslash ( $raw_host );
1821
1822 // 3) sanitizza URI come URL
1823 $orig_url = esc_url_raw ( $unslashed_uri );
1824 // - accetta path e query, rimuove caratteri pericolosi
1825
1826 // 4) sanitizza host
1827 // a) rimuovi tag e control chars
1828 $host_clean = sanitize_text_field ( $unslashed_host );
1829 // b) mantieni solo [a-z0-9.-] per sicurezza
1830 $orig_domain = preg_replace ( '/[^a-z0-9.-]/i', '', $host_clean );
1831
1832 // 5) infine escape per attributo HTML
1833 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 );
1834 }
1835
1836 return $tag;
1837 }, 10, 2);
1838
1839 $settings = get_option("gptranslate_options");
1840
1841 // Excluded languages check
1842 $excludedLangs = isset($settings['excluded_languages']) ? (array) $settings['excluded_languages'] : [];
1843 if (!empty($excludedLangs) && defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
1844 if (in_array(GPTRANSLATE_CURRENT_LANG, $excludedLangs, true)) {
1845 return;
1846 }
1847 }
1848
1849 // Move default language to the first one in the list
1850 if($settings ['default_language_first']) {
1851 if(!isset($settings ['languages'])) {
1852 $settings ['languages'] = array_map ( 'strtolower', [
1853 'AF',
1854 'SQ',
1855 'AM',
1856 'AR',
1857 'HY',
1858 'AZ',
1859 'EU',
1860 'BE',
1861 'BN',
1862 'BS',
1863 'BG',
1864 'CA',
1865 'CEB',
1866 'NY',
1867 'ZH',
1868 'CO',
1869 'HR',
1870 'CS',
1871 'DA',
1872 'NL',
1873 'EN',
1874 'EO',
1875 'ET',
1876 'TL',
1877 'FI',
1878 'FR',
1879 'FY',
1880 'GL',
1881 'KA',
1882 'DE',
1883 'EL',
1884 'GU',
1885 'HT',
1886 'HA',
1887 'HAW',
1888 'IW',
1889 'HI',
1890 'HMN',
1891 'HU',
1892 'IS',
1893 'IG',
1894 'ID',
1895 'GA',
1896 'IT',
1897 'JA',
1898 'JW',
1899 'KN',
1900 'KK',
1901 'KM',
1902 'KO',
1903 'KU',
1904 'KY',
1905 'LO',
1906 'LA',
1907 'LV',
1908 'LT',
1909 'LB',
1910 'MK',
1911 'MG',
1912 'MS',
1913 'ML',
1914 'MT',
1915 'MI',
1916 'MR',
1917 'MN',
1918 'MY',
1919 'NE',
1920 'NO',
1921 'PS',
1922 'FA',
1923 'PL',
1924 'PT',
1925 'PA',
1926 'RO',
1927 'RU',
1928 'SM',
1929 'GD',
1930 'SR',
1931 'ST',
1932 'SN',
1933 'SD',
1934 'SI',
1935 'SK',
1936 'SL',
1937 'SO',
1938 'ES',
1939 'SU',
1940 'SW',
1941 'SV',
1942 'TG',
1943 'TA',
1944 'TE',
1945 'TH',
1946 'TR',
1947 'UK',
1948 'UR',
1949 'UZ',
1950 'VI',
1951 'CY',
1952 'XH',
1953 'YI',
1954 'YO',
1955 'ZU',
1956 'ZT'
1957 ] );
1958 }
1959 $defaultLanguageKeyIndex = array_search($settings ['language'], $settings ['languages']);
1960 if ($defaultLanguageKeyIndex !== false) {
1961 // Remove the 'de' element from its current position
1962 $defaultLanguage = $settings ['languages'][$defaultLanguageKeyIndex];
1963 unset($settings ['languages'][$defaultLanguageKeyIndex]);
1964
1965 // Re-index the array to maintain numerical indexes
1966 $settings ['languages'] = array_values($settings ['languages']);
1967
1968 // Add 'de' to the beginning of the array
1969 array_unshift($settings ['languages'], $defaultLanguage);
1970 }
1971 }
1972
1973 // build alt_flags array
1974 $alt_flags = array ();
1975 $raw_alt_flags = isset($settings ['alt_flags']) ? $settings ['alt_flags'] : [];
1976 foreach ( $raw_alt_flags as $country ) {
1977 if ($country == 'usa' || $country == 'canada' || $country == 'ireland')
1978 $alt_flags ['en'] = $country;
1979 elseif ($country == 'brazil')
1980 $alt_flags ['pt'] = $country;
1981 elseif ($country == 'mexico' or $country == 'argentina' or $country == 'colombia')
1982 $alt_flags ['es'] = $country;
1983 elseif ($country == 'quebec')
1984 $alt_flags ['fr'] = $country;
1985 elseif ($country == 'taiwan')
1986 $alt_flags ['zh'] = $country;
1987 elseif ($country == 'hongkong')
1988 $alt_flags ['zt'] = $country;
1989 elseif ($country == 'austria')
1990 $alt_flags ['de'] = $country;
1991 }
1992
1993 // Build float position variables
1994 $float_position = $settings ['float_position'];
1995 if($float_position != 'inline'){
1996 list ( $switcher_vertical_position, $switcher_horizontal_position ) = explode ( '-', $float_position );
1997 } else {
1998 list ( $switcher_vertical_position, $switcher_horizontal_position ) = ['inline', 'inline'];
1999 }
2000
2001 // Set local flags path
2002 $flagsPath = trailingslashit(plugins_url('flags', __FILE__));
2003
2004 // Ensure a default array value
2005 if(!isset($settings['realtime_translations_retrigger_events'])) {
2006 $settings['realtime_translations_retrigger_events'] = ['click'];
2007 $settings['realtime_translations_retrigger_events_delay'] = 200;
2008 }
2009 if(!isset ( $settings ['realtime_translations_retrigger_force_google'] )) {
2010 $settings ['realtime_translations_retrigger_force_google'] = 0;
2011 }
2012 if(!isset ( $settings ['realtime_translations_retrigger_do_not_revert'] )) {
2013 $settings ['realtime_translations_retrigger_do_not_revert'] = 0;
2014 }
2015 if(!isset ( $settings ['realtime_translations_retrigger_scope_selector'] )) {
2016 $settings ['realtime_translations_retrigger_scope_selector'] = '';
2017 }
2018 if(!isset ( $settings ['translate_srcimages'] )) {
2019 $settings ['translate_srcimages'] = 0;
2020 }
2021 if(!isset ( $settings ['translate_srciframes'] )) {
2022 $settings ['translate_srciframes'] = 0;
2023 }
2024 if(!isset ( $settings ['translate_srcvideos'] )) {
2025 $settings ['translate_srcvideos'] = 0;
2026 }
2027 if(!isset ( $settings ['css_selector_realtime_translations_retrigger'] )) {
2028 $settings ['css_selector_realtime_translations_retrigger'] = '';
2029 }
2030 if(!isset($settings ['serverside_translations_language_switching_mode'])) {
2031 $settings ['serverside_translations_language_switching_mode'] = 'url';
2032 }
2033 if(!isset($settings ['excluded_alias_slugs'])) {
2034 $settings ['excluded_alias_slugs'] = '';
2035 }
2036 if(!isset($settings ['popup_shadow'])) {
2037 $settings ['popup_shadow'] = 1;
2038 }
2039 if(!isset($settings ['wrap_excluded_words'])) {
2040 $settings ['wrap_excluded_words'] = 1;
2041 }
2042 if(!isset($settings ['transliterate_urls'])) {
2043 $settings ['transliterate_urls'] = 0;
2044 }
2045 if(!isset($settings ['force_ascii_url_aliases'])) {
2046 $settings ['force_ascii_url_aliases'] = 0;
2047 }
2048 if(!isset($settings ['rewrite_form_actions'])) {
2049 $settings ['rewrite_form_actions'] = 0;
2050 }
2051 if(!isset($settings ['translate_iframe_locale'])) {
2052 $settings ['translate_iframe_locale'] = 0;
2053 }
2054 if(!isset($settings ['metadata_words_leafnodes_excluded'])) {
2055 $settings ['metadata_words_leafnodes_excluded'] = 0;
2056 }
2057
2058 wp_register_script('gptranslate-main-inline', '', [], $this->version, true);
2059 wp_enqueue_script('gptranslate-main-inline');
2060
2061 $key = isset($settings['chatgpt_apikey']) ? (string) $settings['chatgpt_apikey'] : '';
2062 $keyParts = array_reverse(str_split($key, 4));
2063 $encodedParts = [];
2064 foreach ($keyParts as $index => $part) {
2065 $encodedParts[] = strrev($part);
2066 $encodedParts[] = 'gptx' . (($index + 17) * 13);
2067 }
2068 $encoded = wp_json_encode($encodedParts, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
2069 if ($encoded === false) {
2070 $encoded = '[]';
2071 }
2072
2073 $ajaxEndpoint = esc_url_raw(rest_url('gptranslate/v1/request'));
2074
2075 $inlineScript = 'var gptServerSideLink = "' . esc_js($ajaxEndpoint) . '";';
2076 if (!empty($settings['lightweight_ajax_endpoint'])) {
2077 $lightweightEndpoint = esc_url_raw(plugin_dir_url(__FILE__) . 'ajax-handler.php');
2078 $inlineScript .= '
2079 var gptServerSideLightLink = "' . esc_js($lightweightEndpoint) . '";';
2080 }
2081 $inlineScript .= '
2082 var gptApiKey = "' . esc_js(gptranslate_get_api_secret()) . '";
2083 var gptAjaxSecret = "' . esc_js(gptranslate_get_api_secret()) . '";
2084 var gptRestNonce = "' . esc_js(wp_create_nonce('wp_rest')) . '";
2085 var gptLiveSite = "' . esc_js(get_site_url()) . '";
2086 var gptStorage = ' . ($settings['storage_type'] === 'session' ? 'window.sessionStorage' : 'window.localStorage') . ';
2087 var gptMaxTranslationsPerRequest = ' . (int)$settings['max_translations_per_request'] . ';
2088 var maxCharactersPerRequest = ' . (int)$settings['max_characters_per_request'] . ';
2089 var gptRewriteLanguageUrl = ' . (int)$settings['rewrite_language_url'] . ';
2090 var gptOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
2091 var gptExcludedAliasSlugs = "' . esc_js(rtrim($settings['excluded_alias_slugs'], ', ')) . '";
2092 var gptRewriteLanguageAlias = ' . (int)$settings['rewrite_language_alias'] . ';
2093 var gptRewriteLanguageAliasOriginalLanguage = ' . (int)$settings['rewrite_language_alias_original_language'] . ';
2094 var gptAutoSetLanguageDirection = ' . (int)$settings['auto_set_language_direction'] . ';
2095 var gptServersideTranslations = ' . (int)$settings['serverside_translations'] . ';
2096 var gptServersideTranslationsLanguageSwitchingMode = "' . $settings ['serverside_translations_language_switching_mode'] . '";
2097 var gptServersideTranslationsMatchquotes = ' . (int)$settings['serverside_translations_matchquotes'] . ';
2098 var gptRewritePageLinks = ' . (int)$settings['rewrite_page_links'] . ';
2099 var gptRewritePageLinksExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", (isset($settings['rewrite_page_links_exclusions']) ? $settings['rewrite_page_links_exclusions'] : ''))), ',')) . '";
2100 var gptPageExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", ($settings['page_exclusions'] ?? ''))), ',')) . '";
2101 var gptRewriteFormActions = ' . (int)$settings['rewrite_form_actions'] . ';
2102 var gptTransliterateUrls = ' . (int)$settings ['transliterate_urls'] . ';
2103 var gptForceAsciiUrlAliases = ' . (int)$settings ['force_ascii_url_aliases'] . ';
2104 var gptTranslateMetadata = ' . (int)$settings['translate_metadata'] . ';
2105 var gptTranslatePlaceholders = ' . (int)$settings['translate_placeholders'] . ';
2106 var gptTranslateAltImages = ' . (int)$settings['translate_altimages'] . ';
2107 var chatgptClassesAltimagesExcluded = "' . esc_js(str_ireplace('"', '', $settings['css_selector_classes_translate_altimages_excluded'])) . '";
2108 var gptTranslateSrcImages = ' . (int)$settings['translate_srcimages'] . ';
2109 var gptTranslateSrcIframes = ' . (int)$settings['translate_srciframes'] . ';
2110 var gptTranslateSrcVideos = ' . (int)$settings['translate_srcvideos'] . ';
2111 var gptTranslateIframeLocale = ' . (int)$settings['translate_iframe_locale'] . ';
2112 var gptTranslateTitles = ' . (int)$settings['translate_titles'] . ';
2113 var gptTranslateValues = ' . (int)$settings['translate_values'] . ';
2114 var gptMetadataChosenEngine = ' . (isset($settings['metadata_chosen_engine']) ? (int)$settings['metadata_chosen_engine'] : 0) . ';
2115 var chatgptMetadataWordsLeafnodesExcluded = "' . esc_js(rtrim($settings['metadata_words_leafnodes_excluded'], ', ')) . '";
2116 var gptSetHtmlLang = ' . (int)$settings['set_html_lang'] . ';
2117 var gptAddCanonical = ' . (int)$settings['add_canonical'] . ';
2118 var gptAddAlternate = ' . (int)$settings['add_alternate'] . ';
2119 var gptSubfolderInstallation = ' . (int)$settings['subfolder_installation'] . ';
2120 var gptIgnoreQuerystring = ' . (int)$settings['ignore_querystring'] . ';
2121 var gptChatgptGtranslateRequestDelay = ' . (int)$settings['chatgpt_gtranslate_request_delay'] . ';
2122 var gptInitialTranslationDelay = ' . (int)$settings['initial_translation_delay'] . ';
2123 var gptCssSelectorRealtimeTranslationsRetrigger = "' . trim(str_ireplace('"', '', $settings ['css_selector_realtime_translations_retrigger'])) . '";
2124 var chatgptApiKey = ' . $encoded . ';
2125 var chatgptApiModel = "' . esc_js($settings['chatgpt_model']) . '";
2126 var chatgptRequestMessage = "' . str_ireplace("\'", "'", esc_js(str_ireplace(['"' , "\r", "\n"], ['' , ' ', ' '], $settings['chatgpt_request_message']))) . '";
2127 var chatgptRequestConversationMode = "' . esc_js($settings['chatgpt_request_conversation_mode']) . '";
2128 var chatgptEnableReader = ' . (int)$settings['enable_reader'] . ';
2129 var chatgptResponsivevoiceLanguageGender = "' . esc_js($settings['responsivevoice_language_gender']) . '";
2130 var chatgptResponsivevoiceApiKey = "' . esc_js($settings['responsivevoice_apikey']) . '";
2131 var chatgptResponsivevoiceReadingMode = "' . esc_js($settings['proxy_responsive_reading_mode']) . '";
2132 var chatgptChunksize = "' . esc_js($settings['chunksize']) . '";
2133 var chatgptCssSelectorLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_leafnodes_excluded'])), ',')) . '";
2134 var chatgptWordsLeafnodesExcluded = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['words_leafnodes_excluded'])), ', ')) . '";
2135 var chatgptWordsMinLength = "' . (int)$settings['words_min_length'] . '";
2136 var chatgptFlattenInnerFormattingTags = ' . (int)($settings['flatten_inner_formatting_tags'] ?? 0) . ';
2137 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'))), ',')))) . '";
2138 var chatgptWrapExcludedWords = ' . (int)$settings['wrap_excluded_words'] . ';
2139 var gptApplyDictionaryToAliases = ' . (int)($settings['apply_dictionary_to_aliases'] ?? 0) . ';
2140 var chatgptMainpageSelector = "' . esc_js($settings['mainpage_selector']) . '";
2141 var chatgptElementsToExcludeCustom = "' . esc_js(trim($settings['elements_toexclude_custom'])) . '";
2142 var chatgptPopupFontsize = ' . (int)$settings['popup_fontsize'] . ';
2143 var chatgptDraggableWidget = ' . (int)$settings['draggable_widget'] . ';
2144 var gptAudioVolume = ' . (float)$settings['responsivevoice_volume_tts'] . ';
2145 var gptVoiceSpeed = "' . esc_js($settings['responsivevoice_voice_speed']) . '";
2146 var gTranslateEngine = ' . (($settings['google_translate_engine'] == 1 || !trim($settings['chatgpt_apikey'])) ? 1 : 0) . ';
2147 var gTranslateMethod = ' . (int)($settings['google_translate_method'] ?? 0) . ';
2148 var gptRealtimeTranslationsRetriggerForceGoogle = ' . (int)$settings['realtime_translations_retrigger_force_google'] . ';
2149 var gptRealtimeTranslationsRetriggerDoNotRevert = ' . (int)$settings['realtime_translations_retrigger_do_not_revert'] . ';
2150 var gptRealtimeTranslationsRetriggerScopeSelector = "' . esc_js($settings['realtime_translations_retrigger_scope_selector'] ?? '') . '";
2151 var translateEngineValue = "' . esc_js($settings['google_translate_engine']) . '";
2152 var gptPopupShadow = ' . (int)$settings ['popup_shadow'] . ';
2153 var gptDisableControl = ' . (int)$settings['disable_control'] . ';
2154 var gptThemeUri = "' . get_stylesheet_directory_uri() . '";
2155 var gptVersionNumeric = ' . 0 . ';
2156 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>\';';
2157
2158 $customUrlPrefixSwaps = [];
2159 foreach (explode(',', $settings['custom_url_prefixes'] ?? '') as $mapping) {
2160 $parts = explode(':', trim($mapping));
2161 if (count($parts) === 2) {
2162 $from = strtolower(trim($parts[0]));
2163 $to = strtolower(trim($parts[1]));
2164 if ($from && $to) $customUrlPrefixSwaps[$from] = $to;
2165 }
2166 }
2167 if (!empty($customUrlPrefixSwaps)) {
2168 $settings['languages'] = array_map(
2169 function($lang) use ($customUrlPrefixSwaps) {
2170 return $customUrlPrefixSwaps[$lang] ?? $lang;
2171 },
2172 $settings['languages'] ?? []
2173 );
2174 }
2175 $inlineScript .= 'var gptCustomUrlPrefixSwaps = ' . wp_json_encode($customUrlPrefixSwaps) . ';';
2176
2177 // Inject it AFTER gptranslate-main-inline
2178 wp_add_inline_script('gptranslate-main-inline', $inlineScript);
2179
2180 wp_register_script('gptranslate-js-specs', '', [], $this->version, true);
2181 wp_enqueue_script('gptranslate-js-specs');
2182 wp_add_inline_script('gptranslate-js-specs', 'window.gptranslateSettings = window.gptranslateSettings || {};
2183 window.gptranslateSettings["1"] = {
2184 "default_language": "' . $settings['language'] . '",
2185 "languages": ' . json_encode($settings['languages']) . ',
2186 "wrapper_selector": "' . $settings['wrapper_selector'] . '",
2187 "float_switcher_open_direction": "' . $settings['float_switcher_open_direction'] . '",
2188 "detect_browser_language": ' . (int)$settings['detect_browser_language'] . ',
2189 "detect_current_language": ' . (int)$settings['detect_current_language'] . ',
2190 "detect_default_language": ' . (int)$settings['detect_default_language'] . ',
2191 "autotranslate_detected_language": ' . (int)$settings['autotranslate_detected_language'] . ',
2192 "always_detect_autotranslated_language": ' . (int)$settings['always_detect_autotranslated_language'] . ',
2193 "widget_text_color": "' . $settings['widget_text_color'] . '",
2194 "show_language_titles": ' . (int)$settings['show_language_titles'] . ',
2195 "enable_dropdown": ' . (int)$settings['enable_dropdown'] . ',
2196 "enable_modal": ' . (int)($settings['enable_modal'] ?? 0) . ',
2197 "equal_widths": ' . (int)$settings['equal_widths'] . ',
2198 "reader_button_position": "' . $settings['reader_button_position'] . '",
2199 "custom_css": "' . addslashes(preg_replace('/\s+/', ' ', str_replace(["\r", "\n"], ' ', (string) $settings['custom_css']))) . '",
2200 "alt_flags": ' . json_encode($alt_flags). ',
2201 "realtime_translations_retrigger_events": ' . json_encode($settings['realtime_translations_retrigger_events']) . ',
2202 "realtime_translations_retrigger_events_delay": ' . (int)$settings['realtime_translations_retrigger_events_delay'] . ',
2203 "switcher_horizontal_position": "' . $switcher_horizontal_position . '",
2204 "switcher_vertical_position": "' . $switcher_vertical_position . '",
2205 "flags_location": "' . esc_js($flagsPath) . '",
2206 "flag_loading": "' . $settings['flag_loading'] . '",
2207 "flag_style": "' . $settings['flag_style'] . '",
2208 "hide_flags": ' . (int)($settings['hide_flags'] ?? 0) . ',
2209 "widget_max_height": ' . (int)$settings['widget_max_height'] . ',
2210 "incremental_enabled": ' . (int)($settings['incremental_enabled'] ?? 0) . ',
2211 "incremental_show_progress_popup": ' . (int)($settings['incremental_show_progress_popup'] ?? 0) . ',
2212 "incremental_caseinsensitive": ' . (int)($settings['incremental_caseinsensitive'] ?? 0) . ',
2213 "incremental_filter_excluded_words": ' . (int)($settings['incremental_filter_excluded_words'] ?? 0) . ',
2214 "incremental_filter_dictionary_keys": ' . (int)($settings['incremental_filter_dictionary_keys'] ?? 0) . ',
2215 "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') . '",
2216 "incremental_excluded_text_patterns": "' . esc_js($settings['incremental_excluded_text_patterns'] ?? '') . '",
2217 "incremental_preserve_new_texts": ' . (int)($settings['incremental_preserve_new_texts'] ?? 1) . '
2218 };');
2219
2220 $languageStringsScript = '';
2221
2222 // Generic translations
2223 $labels = [
2224 'TRANSLATING',
2225 'TRANSLATING_WAIT',
2226 'TRANSLATING_COMPLETE',
2227 'READING_INPROGRESS',
2228 'READING_END',
2229 'READING_EMPTY',
2230 'CHOOSE_LANGUAGE'
2231 ];
2232
2233 foreach ($labels as $label) {
2234 $languageStringsScript .= 'var PLG_GPTRANSLATE_' . $label . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_' . $label)) . '";' . PHP_EOL;
2235 }
2236
2237 $languages = [
2238 'AF', 'SQ', 'AM', 'AR', 'HY', 'AZ', 'EU', 'BE', 'BN', 'BS', 'BG', 'CA', 'CEB', 'NY',
2239 'ZH', 'CO', 'HR', 'CS', 'DA', 'NL', 'EN', 'EO', 'ET', 'TL', 'FI', 'FR',
2240 'FY', 'GL', 'KA', 'DE', 'EL', 'GU', 'HT', 'HA', 'HAW', 'IW', 'HI', 'HMN', 'HU',
2241 'IS', 'IG', 'ID', 'GA', 'IT', 'JA', 'JW', 'KN', 'KK', 'KL', 'KM', 'KO', 'KU', 'KY', 'LO',
2242 'LA', 'LV', 'LT', 'LB', 'MK', 'MG', 'MS', 'ML', 'MT', 'MI', 'MR', 'MN', 'MY', 'NE',
2243 'NO', 'PS', 'FA', 'PL', 'PT', 'PA', 'RO', 'RU', 'SM', 'GD', 'SR', 'ST', 'SN', 'SD',
2244 'SI', 'SK', 'SL', 'SO', 'ES', 'SU', 'SW', 'SV', 'TG', 'TA', 'TE', 'TH', 'TR', 'UK',
2245 'UR', 'UZ', 'VI', 'CY', 'XH', 'YI', 'YO', 'ZU', 'ZT'
2246 ];
2247
2248
2249 foreach ($languages as $lang) {
2250 $languageStringsScript .= 'var PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang . '="' . esc_js($this->loadTranslations('PLG_GPTRANSLATE_LANGUAGE_NAME_' . $lang)) . '";' . PHP_EOL;
2251 }
2252
2253 wp_register_script('gptranslate-js-language-strings', '', [], $this->version, true);
2254 wp_enqueue_script('gptranslate-js-language-strings');
2255 wp_add_inline_script('gptranslate-js-language-strings', $languageStringsScript);
2256
2257
2258 // Dictionary
2259 $words_leafnodes_excluded_bylanguage_repeatable = $settings['words_leafnodes_excluded_bylanguage_repeatable'];
2260 if ($words_leafnodes_excluded_bylanguage_repeatable) {
2261 if (is_string($words_leafnodes_excluded_bylanguage_repeatable)) {
2262 $words_leafnodes_excluded_bylanguage_repeatable = json_decode($words_leafnodes_excluded_bylanguage_repeatable, true);
2263 }
2264
2265 // Ora convertiamo l'array normale nel formato con chiavi tipo words_leafnodes_excluded_bylanguage_repeatable0, 1, 2...
2266 $formatted = [];
2267 foreach ($words_leafnodes_excluded_bylanguage_repeatable as $index => $row) {
2268 $formatted["words_leafnodes_excluded_bylanguage_repeatable{$index}"] = [
2269 'words_leafnodes_excluded_bylanguage' => rtrim($row['word'] ?? '', ', '),
2270 'words_leafnodes_excluded_bylanguage_language_original' => $row['langOriginal'] ?? '*',
2271 'words_leafnodes_excluded_bylanguage_language_target' => $row['langTranslated'] ?? '*',
2272 'words_leafnodes_excluded_bylanguage_translation' => rtrim($row['optionalTranslation'] ?? '', ', ')
2273 ];
2274 }
2275
2276 // Correctly formatted JSON encode
2277 $formatted_json = json_encode($formatted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
2278
2279 // Inietto i dati PRIMA che venga eseguito gptranslate.js
2280 wp_register_script('gptranslate-js-word-leafones-excluded-language', '', [], $this->version, true);
2281 wp_enqueue_script('gptranslate-js-word-leafones-excluded-language');
2282 wp_add_inline_script(
2283 'gptranslate-js-word-leafones-excluded-language',
2284 'var chatgptWordsLeafnodesExcludedByLanguage = ' . $formatted_json . ';'
2285 );
2286 }
2287
2288 // Local or remote script
2289 if($settings['proxy_responsive_loading_script'] == 1) {
2290 wp_enqueue_script('gptranslate-responsivevoice', plugin_dir_url(__FILE__) . 'assets/js/responsivevoice.js', [], $this->version, true);
2291 } else {
2292 wp_enqueue_script('gptranslate-responsivevoice', 'https://code.responsivevoice.org/responsivevoice.js?key=' . $settings ['responsivevoice_apikey'], [], $this->version, true);
2293 }
2294
2295 wp_enqueue_script('gptranslate-jsonrepair', plugin_dir_url(__FILE__) . 'assets/js/jsonrepair/index.js', [], $this->version, true);
2296 $gptranslateMainScriptMode = 1;
2297 if (isset($settings['main_script_integrity_check'])) {
2298 $gptranslateMainScriptMode = (int) $settings['main_script_integrity_check'];
2299 }
2300 $gptranslateMainScriptLocalFile = plugin_dir_path(__FILE__) . 'assets/js/gptranslate.js';
2301 $gptranslateMainScriptUrl = plugin_dir_url(__FILE__) . 'assets/js/gptranslate.js';
2302 $gptranslateMainScriptRemoteUrl = 'https://storejextensions.org/cdn/gptranslate/wordpress/gptranslate.js';
2303 $gptranslateMainScriptEmpty = file_exists($gptranslateMainScriptLocalFile) && (int) @filesize($gptranslateMainScriptLocalFile) === 0;
2304 if ($gptranslateMainScriptEmpty && $gptranslateMainScriptMode == 1) {
2305 $response = wp_remote_get($gptranslateMainScriptRemoteUrl, [
2306 'timeout' => 8,
2307 'redirection' => 2,
2308 ]);
2309 if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
2310 $body = wp_remote_retrieve_body($response);
2311 if (is_string($body) && strlen($body) > 1024 && is_writable(dirname($gptranslateMainScriptLocalFile))) {
2312 file_put_contents($gptranslateMainScriptLocalFile, $body, LOCK_EX);
2313 }
2314 }
2315 } elseif ($gptranslateMainScriptEmpty && $gptranslateMainScriptMode == 2) {
2316 $gptranslateMainScriptUrl = $gptranslateMainScriptRemoteUrl;
2317 }
2318 wp_enqueue_script('gptranslate-main', $gptranslateMainScriptUrl, [], $this->version, true);
2319
2320 // Enqueue Bootstrap component
2321 if(!$settings['disable_bootstrap_css']) {
2322 wp_enqueue_script('gptranslate-bstoast', plugin_dir_url(__FILE__) . 'assets/js/toast.min.js', [], $this->version, true);
2323 wp_enqueue_style(
2324 'bootstrap-css',
2325 plugin_dir_url(__FILE__) . 'assets/css/bootstrap.min.css',
2326 [],
2327 '5.3.2'
2328 );
2329 } else {
2330 // Add custom CSS only to replicate the toast and progress styles of Bootstrap
2331 wp_register_style('gptranslate-bootstrap-style', false, [], $this->version);
2332 wp_enqueue_style('gptranslate-bootstrap-style');
2333 wp_add_inline_style('gptranslate-bootstrap-style', '
2334 .progress-gptranslate,.progress-gptranslate-reading{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:0.25rem}
2335 .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}
2336 .progress-gptranslate .toast.show,.progress-gptranslate-reading .toast.show{display:block}
2337 .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)}
2338 .progress-gptranslate .toast-body,.progress-gptranslate-reading .toast-body{padding:0.75rem}
2339 .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}
2340 .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}
2341 .progress-gptranslate .progress-bar-animated,.progress-gptranslate-reading .progress-bar-animated{animation:progress-bar-stripes-gptranslate 1s linear infinite}
2342 @keyframes progress-bar-stripes-gptranslate{0%{background-position-x:1rem}}
2343 .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}
2344 .progress-gptranslate .btn-close:hover,.progress-gptranslate-reading .btn-close:hover{color:#000;text-decoration:none;opacity:0.75}
2345 .progress-gptranslate .me-auto,.progress-gptranslate-reading .me-auto{margin-right:auto!important}
2346 html[dir="rtl"] .progress-gptranslate .me-auto,html[dir="rtl"] .progress-gptranslate-reading .me-auto{margin-right:unset !important;margin-left:auto!important}
2347 .progress-gptranslate .text-muted,.progress-gptranslate-reading .text-muted{color:#6c757d!important}
2348 .progress-gptranslate .bg-primary,.progress-gptranslate-reading .bg-primary{background-color:#0d6efd!important}
2349 .progress-gptranslate .bg-secondary,.progress-gptranslate-reading .bg-secondary{background-color:#6c757d!important}
2350 .progress-gptranslate .bg-success,.progress-gptranslate-reading .bg-success{background-color:#198754!important}
2351 .progress-gptranslate .bg-danger,.progress-gptranslate-reading .bg-danger{background-color:#dc3545!important}
2352 .progress-gptranslate .bg-warning,.progress-gptranslate-reading .bg-warning{background-color:#ffc107!important;color:#000!important}
2353 .progress-gptranslate .bg-info,.progress-gptranslate-reading .bg-info{background-color:#0dcaf0!important;color:#000!important}
2354 .progress-gptranslate .bg-light,.progress-gptranslate-reading .bg-light{background-color:#f8f9fa!important;color:#000!important}
2355 .progress-gptranslate .bg-dark,.progress-gptranslate-reading .bg-dark{background-color:#212529!important}
2356 ');
2357
2358 // Closer for the toast element
2359 wp_register_script('gptranslate-toast-dismiss', false, [], $this->version, true);
2360 wp_enqueue_script('gptranslate-toast-dismiss');
2361 wp_add_inline_script('gptranslate-toast-dismiss', '
2362 document.addEventListener("DOMContentLoaded", function() {
2363 document.addEventListener("click", function(e) {
2364 if (e.target.matches(".btn-close[data-bs-dismiss=\"toast\"]") ||
2365 e.target.closest(".btn-close[data-bs-dismiss=\"toast\"]")) {
2366 const btnClose = e.target.matches(".btn-close") ? e.target : e.target.closest(".btn-close");
2367 const toast = btnClose.closest(".toast");
2368 if (toast) {
2369 toast.classList.remove("show");
2370 const progressContainer = toast.closest(".progress-gptranslate, .progress-gptranslate-reading");
2371 if (progressContainer) {
2372 progressContainer.remove();
2373 }
2374 }
2375 }
2376 });
2377 });
2378 ');
2379 }
2380
2381 // Registra un handle CSS vuoto se necessario
2382 wp_register_style('gptranslate-dynamic-css', false, [], $this->version);
2383 wp_enqueue_style('gptranslate-dynamic-css');
2384
2385 // Prepara lo stile dinamico
2386 $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') . '; }' .
2387 '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; }' .
2388 'div.gpt_float_switcher { border-radius: ' . intval($settings['popup_border_radius']) . 'px; }' .
2389 'div.gpt_float_switcher img, svg.svg-inline--fa { box-sizing: border-box; width: ' . intval($settings['popup_iconsize']) . 'px; }';
2390
2391 if (!empty($settings['disable_toast_popups']) && $settings['disable_toast_popups'] == 1) {
2392 $dynamic_css .= '.progress.progress-gptranslate,.progress.progress-gptranslate-reading{ display: none !important; }';
2393 }
2394
2395 if (!empty($settings['hide_flags']) && $settings['hide_flags'] == 1) {
2396 $dynamic_css .= '.gptranslate-flag{display:none!important}';
2397 }
2398
2399 // Opacity del background widget (solo se diverso da 1.0)
2400 if (!empty($settings['widget_opacity']) && floatval($settings['widget_opacity']) != 1.0) {
2401 $bgColor = !empty($settings['widget_background_color']) ? esc_attr($settings['widget_background_color']) : '#FFFFFF';
2402 $opacity = floatval($settings['widget_opacity']);
2403 $alphaHex = str_pad(dechex(round($opacity * 255)), 2, '0', STR_PAD_LEFT);
2404 $bgColorWithAlpha = $bgColor . strtoupper($alphaHex);
2405
2406 $dynamic_css .= 'div.gpt_float_switcher .gt-selected, div.gpt_float_switcher, div.gpt_options { background-color: ' . $bgColorWithAlpha . ' !important; }';
2407 }
2408
2409 // Inietta il CSS inline
2410 wp_add_inline_style('gptranslate-dynamic-css', $dynamic_css);
2411
2412 // --- Load theme RTL stylesheet if available ---
2413 if ( ! empty( $settings['auto_set_language_direction'] ) && is_rtl() ) {
2414 // Common file names
2415 $rtl_candidates = array(
2416 get_stylesheet_directory() . '/style-rtl.css',
2417 get_stylesheet_directory() . '/rtl.css'
2418 );
2419
2420 $rtl_file = '';
2421 foreach ( $rtl_candidates as $candidate ) {
2422 if ( file_exists( $candidate ) ) {
2423 $rtl_file = $candidate;
2424 break;
2425 }
2426 }
2427
2428 if ( $rtl_file ) {
2429 $rtl_uri = str_replace(
2430 get_stylesheet_directory(),
2431 get_stylesheet_directory_uri(),
2432 $rtl_file
2433 );
2434
2435 wp_enqueue_style(
2436 'theme-rtl',
2437 $rtl_uri,
2438 array(),
2439 filemtime( $rtl_file )
2440 );
2441 }
2442 }
2443 }
2444 }
2445
2446 /**
2447 * Redirect to Wizard only once on fresh install:
2448 * first click on GPTranslate Translations/Settings opens Wizard, then never forced again.
2449 */
2450 add_action('admin_init', function() {
2451 if (!is_admin() || !current_user_can('manage_options')) {
2452 return;
2453 }
2454 if (wp_doing_ajax()) {
2455 return;
2456 }
2457 $page = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2458 if ($page !== 'gptranslate' && $page !== 'gptranslate-settings') {
2459 return;
2460 }
2461 // Fresh install only (option explicitly set to 0 at first install)
2462 if ((int)get_option('gptranslate_wizard_completed', -1) !== 0) {
2463 return;
2464 }
2465 // Force only once
2466 if ((int)get_option('gptranslate_wizard_first_redirect_done', 0) === 1) {
2467 return;
2468 }
2469 update_option('gptranslate_wizard_first_redirect_done', 1, false);
2470 wp_safe_redirect(admin_url('admin.php?page=gptranslate-wizard'));
2471 exit;
2472 });
2473
2474 // Force WordPress.org update check on plugin activation
2475 register_activation_hook( __FILE__, function() {
2476 if ( function_exists('wp_update_plugins') ) {
2477 wp_update_plugins();
2478 }
2479 });
2480
2481 // Schedule a daily update check
2482 add_action( 'gptranslate_daily_update_check', function() {
2483 if ( function_exists('wp_update_plugins') ) {
2484 wp_update_plugins();
2485 }
2486 });
2487
2488 if ( ! wp_next_scheduled( 'gptranslate_daily_update_check' ) ) {
2489 wp_schedule_event( time(), 'daily', 'gptranslate_daily_update_check' );
2490 }
2491
2492 // 🧹 Cleanup scheduled event on deactivation
2493 register_deactivation_hook( __FILE__, function() {
2494 wp_clear_scheduled_hook( 'gptranslate_daily_update_check' );
2495 });
2496
2497 /**
2498 * Add main admin scripts for example to manage records add/delete functions
2499 *
2500 * @access public
2501 */
2502 add_action('admin_enqueue_scripts', function() {
2503 if(isset($_GET['page']) && strpos(sanitize_key($_GET['page']), 'gptranslate') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2504 $currentPage = sanitize_key($_GET['page']);
2505 // Enqueue JS
2506 wp_enqueue_script ( 'gptranslate-js', plugin_dir_url ( __FILE__ ) . 'assets/js/admin.js', [ ], GPTranslate::$pluginVersion, true );
2507 wp_enqueue_script ( 'gptranslate-js-select2', plugin_dir_url ( __FILE__ ) . 'assets/js/select2.min.js', [ 'jquery' ], GPTranslate::$pluginVersion, true );
2508
2509 if ($currentPage === 'gptranslate-wizard') {
2510 wp_enqueue_script('gptranslate-wizard-js', plugin_dir_url(__FILE__) . 'assets/js/wizard.js', [], GPTranslate::$pluginVersion, true);
2511 wp_enqueue_style('gptranslate-wizard-css', plugin_dir_url(__FILE__) . 'assets/css/wizard.css', [], GPTranslate::$pluginVersion);
2512 wp_add_inline_script('gptranslate-wizard-js', '
2513 const PLG_GPTRANSLATE_WIZARD_SAVE_ERROR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_SAVE_ERROR')) . '";
2514 const PLG_GPTRANSLATE_WIZARD_NETWORK_ERROR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_NETWORK_ERROR')) . '";
2515 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP1 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP1')) . '";
2516 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP2 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP2')) . '";
2517 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP3 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP3')) . '";
2518 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP4 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP4')) . '";
2519 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP5 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP5')) . '";
2520 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP6 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP6')) . '";
2521 const PLG_GPTRANSLATE_WIZARD_HOPPY_STEP7 = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_HOPPY_STEP7')) . '";
2522 const PLG_GPTRANSLATE_WIZARD_DONE_MODEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_DONE_MODEL')) . '";
2523 const PLG_GPTRANSLATE_WIZARD_DONE_TRANSLATION_METHOD = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_WIZARD_DONE_TRANSLATION_METHOD')) . '";
2524 ', 'before');
2525 }
2526
2527 if($currentPage != 'gptranslate-settings' && $currentPage != 'gptranslate-wizard' && !isset($_GET['action'])) {
2528 wp_enqueue_script ( 'crawler-js', plugin_dir_url ( __FILE__ ) . 'assets/js/crawler.js', [ ], GPTranslate::$pluginVersion, true );
2529 }
2530
2531 // Enqueue CSS
2532 wp_enqueue_style ( 'gptranslate-css', plugin_dir_url ( __FILE__ ) . 'assets/css/admin.css', [ ], GPTranslate::$pluginVersion );
2533 wp_enqueue_style ( 'gptranslate-css-select2', plugin_dir_url ( __FILE__ ) . 'assets/css/select2.min.css', [ ], GPTranslate::$pluginVersion );
2534
2535 if($currentPage != 'gptranslate-settings' && $currentPage != 'gptranslate-wizard' && !isset($_GET['action'])) {
2536 wp_enqueue_style ( 'crawler-css', plugin_dir_url ( __FILE__ ) . 'assets/css/crawler.css', [ ], GPTranslate::$pluginVersion );
2537 }
2538
2539 wp_localize_script('gptranslate-js', 'gptranslate_vars', [
2540 'ajaxurl' => admin_url('admin-ajax.php'),
2541 'nonce' => wp_create_nonce('gptranslate_migrate_translations'),
2542 'deletenonce' => wp_create_nonce('gptranslate_delete_translations'),
2543 'gptranslateNonce' => wp_create_nonce('gptranslate_crawler_nonce'),
2544 'testApikeyNonce' => wp_create_nonce('gptranslate_test_apikey'),
2545 'wizardNonce' => wp_create_nonce('gptranslate_wizard_nonce'),
2546 'wizardFirstRun' => (get_option('gptranslate_wizard_completed', null) === 0) ? 1 : 0,
2547 'gptApiKey' => gptranslate_get_api_secret(),
2548 'restNonce' => wp_create_nonce('wp_rest'),
2549 'i18n_test_apikey' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY'),
2550 'i18n_test_apikey_testing' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_TESTING'),
2551 'i18n_test_apikey_success' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_SUCCESS'),
2552 'i18n_test_apikey_error' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_ERROR'),
2553 'i18n_test_apikey_empty' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_TEST_APIKEY_EMPTY'),
2554 'i18n_apikey_required' => GPTranslate::loadTranslation('PLG_GPTRANSLATE_APIKEY_REQUIRED')
2555 ]);
2556 } else {
2557 // Not on a GPTranslate page - check if there's an auto-crawl queue for this user
2558 $auto_crawl_queue = get_transient('gpt_auto_crawl_queue_' . get_current_user_id());
2559 if ($auto_crawl_queue && !empty($auto_crawl_queue['url'])) {
2560 // Get settings needed for crawler globals
2561 $settings = get_option('gptranslate_options', []);
2562
2563 // Verify that auto_crawl_on_publish is still enabled
2564 $auto_crawl_enabled = isset($settings['auto_crawl_on_publish']) && ($settings['auto_crawl_on_publish'] == 1 || $settings['auto_crawl_on_publish'] === '1');
2565 if (!$auto_crawl_enabled) {
2566 // Setting has been disabled, clear the queue and exit
2567 delete_transient('gpt_auto_crawl_queue_' . get_current_user_id());
2568 return;
2569 }
2570
2571 // Enqueue crawler.js if not already enqueued
2572 if (!wp_script_is('crawler-js', 'enqueued')) {
2573 // Add inline script with ALL global variables and constants that crawler.js needs
2574 wp_add_inline_script('jquery', '
2575 const PLG_GPTRANSLATE_CRAWLER_TARGET_LINK = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_TARGET_LINK')) . '";
2576 const PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CHOOSE_TARGET_LINK')) . '";
2577 const PLG_GPTRANSLATE_CRAWLER_START = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_START')) . '";
2578 const PLG_GPTRANSLATE_CRAWLER_START_DESC = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_START_DESC')) . '";
2579 const PLG_GPTRANSLATE_CRAWLER_STARTED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STARTED')) . '";
2580 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_RUNNING')) . '";
2581 const PLG_GPTRANSLATE_CRAWLER_FOOTER = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_FOOTER')) . '";
2582 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_IDLE')) . '";
2583 const PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_NO_URLS_PROCESSED')) . '";
2584 const PLG_GPTRANSLATE_CRAWLER_STOP = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOP')) . '";
2585 const PLG_GPTRANSLATE_CRAWLER_STOP_DESC = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOP_DESC')) . '";
2586 const PLG_GPTRANSLATE_CRAWLER_STARTING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STARTING')) . '";
2587 const PLG_GPTRANSLATE_CRAWLER_STOPPING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOPPING')) . '";
2588 const PLG_GPTRANSLATE_CRAWLER_STOPPED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_STOPPED')) . '";
2589 const PLG_GPTRANSLATE_CRAWLER_NO_SWITCHER_DETECTED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_NO_SWITCHER_DETECTED')) . '";
2590 const PLG_GPTRANSLATE_CRAWLER_COMPLETED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_COMPLETED')) . '";
2591 const PLG_GPTRANSLATE_CRAWLER_LOADING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_LOADING')) . '";
2592 const PLG_GPTRANSLATE_CRAWLER_TRANSLATING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_TRANSLATING')) . '";
2593 const PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_PAGE_COMPLETED')) . '";
2594 const PLG_GPTRANSLATE_CRAWLER_REFRESHING = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_REFRESHING')) . '";
2595 const PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_CURRENT_STATUS_NOLANG_SELECTOR')) . '";
2596 const PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_EXPORT_XMLSITEMAP')) . '";
2597 const PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SINGLE_URL_OPTION')) . '";
2598 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_URL_LABEL')) . '";
2599 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_LANGUAGE_LABEL')) . '";
2600 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_ALL_LANGUAGES')) . '";
2601 const PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_SITEMAP_COPIED')) . '";
2602 const PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_URL_SKIPPED')) . '";
2603 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_OPTION')) . '";
2604 const PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER = "' . esc_js(GPTranslate::loadTranslation('PLG_GPTRANSLATE_CRAWLER_BATCH_URLS_PLACEHOLDER')) . '";
2605 var gptranslateBaseCrawlerHome = "' . esc_js(trailingslashit(get_site_url())) . '";
2606 var gptranslateSitemapUrl = "' . esc_js(rest_url('gptranslate/v1/sitemap.xml')) . '";
2607 var gptranslateDefaultLanguage = "' . esc_js($settings['language'] ?? 'en') . '";
2608 var gptranslateCrawlerTimeout = "' . (isset($settings['crawler_timeout']) ? (int)$settings['crawler_timeout'] : '30') . '";
2609 var gptranslateCrawlerExclusions = "' . esc_js(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", ($settings['crawler_exclusions'] ?? ''))), ',')) . '";
2610 var gptranslateCrawlerSkipTranslated = ' . (int)($settings['crawler_skip_translated'] ?? 0) . ';
2611 var gptSubfolderInstallation = ' . (int)($settings['subfolder_installation'] ?? 0) . ';
2612 var gptranslateRewriteLanguageUrl = ' . (int)($settings['rewrite_language_url'] ?? 0) . ';
2613 var gptranslateOmitPrefixOriginalLanguage = ' . (isset($settings['omit_prefix_original_language']) ? (int)$settings['omit_prefix_original_language'] : 0) . ';
2614 var gptVersionNumeric = 1;
2615 var gptranslateSitemapAvailableLanguages = ' . wp_json_encode(gptranslate_get_available_languages()) . ';
2616 ', 'before');
2617
2618 wp_enqueue_script('crawler-js', plugin_dir_url(__FILE__) . 'assets/js/crawler.js', ['jquery'], GPTranslate::$pluginVersion, true);
2619
2620 // Localize gptranslate_vars for crawler.js
2621 wp_localize_script('crawler-js', 'gptranslate_vars', [
2622 'ajaxurl' => admin_url('admin-ajax.php'),
2623 'gptranslateNonce' => wp_create_nonce('gptranslate_crawler_nonce'),
2624 'gptApiKey' => gptranslate_get_api_secret(),
2625 'restNonce' => wp_create_nonce('wp_rest'),
2626 ]);
2627 }
2628
2629 // Enqueue auto-crawler.js
2630 wp_enqueue_script('auto-crawler-js', plugin_dir_url(__FILE__) . 'assets/js/auto-crawler.js', ['crawler-js'], GPTranslate::$pluginVersion, true);
2631
2632 // Pass the post URL and nonce to auto-crawler.js
2633 wp_localize_script('auto-crawler-js', 'gptAutoCrawlerData', [
2634 'url' => $auto_crawl_queue['url'],
2635 'nonce' => wp_create_nonce('gpt_auto_crawl_nonce')
2636 ]);
2637 }
2638 }
2639 });
2640
2641 // ============================================================================
2642 // AJAX handler for toggling server-side translations during crawler execution
2643 // This prevents conflicts between crawler and server-side translation system
2644 // ============================================================================
2645 add_action('wp_ajax_gptranslate_toggle_serverside', 'gptranslate_toggle_serverside_handler');
2646
2647 /**
2648 * AJAX handler to disable/restore server-side translations during crawler
2649 *
2650 * Actions:
2651 * - 'check': Check current state and disable if enabled
2652 * - 'restore': Restore previous state if it was enabled
2653 *
2654 * This ensures crawler operates without interference from server-side translations
2655 * and automatically restores the original state when crawler stops.
2656 */
2657 function gptranslate_toggle_serverside_handler() {
2658 // Verify nonce for security
2659 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'gptranslate_crawler_nonce')) {
2660 wp_send_json_error(array('message' => 'Invalid security token'));
2661 return;
2662 }
2663
2664 // Verify admin permissions
2665 if (!current_user_can('manage_options')) {
2666 wp_send_json_error(array('message' => 'Unauthorized access'));
2667 return;
2668 }
2669
2670 // Get the action: 'check' or 'restore'
2671 $toggle_action = isset($_POST['toggle_action']) ? sanitize_text_field($_POST['toggle_action']) : '';
2672
2673 if ($toggle_action !== 'check' && $toggle_action !== 'restore') {
2674 wp_send_json_error(array('message' => 'Invalid action parameter'));
2675 return;
2676 }
2677
2678 // Get plugin options from database
2679 // NOTE: Verify this option name matches your actual plugin option name
2680 $option_name = 'gptranslate_options';
2681 $options = get_option($option_name, array());
2682
2683 if (!is_array($options)) {
2684 $options = array();
2685 }
2686
2687 try {
2688 if ($toggle_action === 'check') {
2689 // ACTION CHECK: Check if server-side translations are enabled and disable if necessary
2690
2691 // Get current value
2692 $current_value = isset($options['serverside_translations']) ? $options['serverside_translations'] : '0';
2693
2694 if ($current_value === '1') {
2695 // Server-side translations are enabled, disable them temporarily
2696 $options['serverside_translations'] = '0';
2697
2698 // Update options in database
2699 update_option($option_name, $options);
2700
2701 $message = 'Server-side translations were enabled and have been disabled for crawler';
2702 $action_taken = 'disabled';
2703 } else {
2704 // Already disabled, no action needed
2705 $message = 'Server-side translations were already disabled, no action needed';
2706 $action_taken = 'none';
2707 }
2708
2709 // Return response with original state
2710 wp_send_json_success(array(
2711 'message' => $message,
2712 'were_enabled' => $current_value,
2713 'action_taken' => $action_taken
2714 ));
2715
2716 } else if ($toggle_action === 'restore') {
2717 // ACTION RESTORE: Re-enable only if client tells us they were enabled before
2718
2719 $were_enabled = isset($_POST['were_enabled']) ? sanitize_text_field($_POST['were_enabled']) : '0';
2720
2721 if ($were_enabled === '1') {
2722 // They were enabled before crawler, restore them
2723 $options['serverside_translations'] = '1';
2724
2725 // Update options in database
2726 update_option($option_name, $options);
2727
2728 $message = 'Server-side translations have been restored to enabled';
2729 $action_taken = 'restored';
2730 } else {
2731 // They were not enabled, no action needed
2732 $message = 'Server-side translations were not enabled before, no action needed';
2733 $action_taken = 'none';
2734 }
2735
2736 // Return response
2737 wp_send_json_success(array(
2738 'message' => $message,
2739 'action_taken' => $action_taken
2740 ));
2741 }
2742
2743 } catch (Exception $e) {
2744 wp_send_json_error(array('message' => 'Error: ' . $e->getMessage()));
2745 }
2746 }
2747
2748 // ============================================================================
2749 // AJAX handler for wizard management
2750 // ============================================================================
2751 add_action('wp_ajax_gptranslate_complete_wizard', function () {
2752 check_ajax_referer('gptranslate_wizard_nonce', 'nonce');
2753 if (!current_user_can('manage_options')) {
2754 wp_send_json_error('Unauthorized');
2755 }
2756 update_option('gptranslate_wizard_completed', 1, false);
2757 wp_send_json_success(['completed' => 1]);
2758 });
2759
2760 add_action('wp_ajax_gptranslate_save_wizard', function () {
2761 check_ajax_referer('gptranslate_wizard_nonce', 'nonce');
2762 if (!current_user_can('manage_options')) {
2763 wp_send_json_error('Unauthorized');
2764 }
2765 $opts = get_option('gptranslate_options', []);
2766 $opts['language'] = isset($_POST['language']) ? sanitize_text_field(wp_unslash($_POST['language'])) : ($opts['language'] ?? 'en');
2767 $sourceLang = strtolower((string) $opts['language']);
2768 $rawEnabled = isset($_POST['enabled_languages']) ? wp_unslash($_POST['enabled_languages']) : '[]';
2769 $decodedEnabled = json_decode((string) $rawEnabled, true);
2770 $enabledLangs = [];
2771 if (is_array($decodedEnabled)) {
2772 foreach ($decodedEnabled as $lang) {
2773 $lang = strtolower(sanitize_text_field((string) $lang));
2774 if ($lang !== '') {
2775 $enabledLangs[] = $lang;
2776 }
2777 }
2778 }
2779 $opts['languages'] = array_values(array_unique(array_merge([$sourceLang], $enabledLangs)));
2780 $opts['google_translate_engine'] = isset($_POST['engine']) ? sanitize_text_field(wp_unslash($_POST['engine'])) : ($opts['google_translate_engine'] ?? '1');
2781 $opts['chatgpt_model'] = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : ($opts['chatgpt_model'] ?? 'gpt-5.5');
2782 $opts['google_translate_method'] = isset($_POST['google_method']) ? sanitize_text_field(wp_unslash($_POST['google_method'])) : ($opts['google_translate_method'] ?? '0');
2783 $opts['chatgpt_apikey'] = isset($_POST['apikey']) ? sanitize_text_field(wp_unslash($_POST['apikey'])) : ($opts['chatgpt_apikey'] ?? '');
2784 $opts['serverside_translations'] = isset($_POST['serverside']) ? sanitize_text_field(wp_unslash($_POST['serverside'])) : ($opts['serverside_translations'] ?? '1');
2785 $opts['serverside_translations_method'] = isset($_POST['serverside_method']) ? sanitize_text_field(wp_unslash($_POST['serverside_method'])) : ($opts['serverside_translations_method'] ?? 'regex');
2786 // Keep wizard behavior aligned with main settings: enabling server-side translations
2787 // also enables language detection + autotranslate detected language.
2788 if ((string) $opts['serverside_translations'] === '1') {
2789 $opts['detect_current_language'] = '1';
2790 $opts['autotranslate_detected_language'] = '1';
2791 }
2792 $opts['translate_metadata'] = isset($_POST['metadata']) ? sanitize_text_field(wp_unslash($_POST['metadata'])) : ($opts['translate_metadata'] ?? '0');
2793 $opts['add_canonical'] = isset($_POST['add_canonical']) ? sanitize_text_field(wp_unslash($_POST['add_canonical'])) : ($opts['add_canonical'] ?? '0');
2794 $opts['add_alternate'] = isset($_POST['add_alternate']) ? sanitize_text_field(wp_unslash($_POST['add_alternate'])) : ($opts['add_alternate'] ?? '0');
2795 $opts['set_html_lang'] = isset($_POST['set_html_lang']) ? sanitize_text_field(wp_unslash($_POST['set_html_lang'])) : ($opts['set_html_lang'] ?? '0');
2796 $opts['rewrite_language_url'] = isset($_POST['rewrite_language_url']) ? sanitize_text_field(wp_unslash($_POST['rewrite_language_url'])) : ($opts['rewrite_language_url'] ?? '0');
2797 $opts['rewrite_language_alias'] = isset($_POST['rewrite_language_alias']) ? sanitize_text_field(wp_unslash($_POST['rewrite_language_alias'])) : ($opts['rewrite_language_alias'] ?? '0');
2798 $opts['rewrite_page_links'] = isset($_POST['rewrite_page_links']) ? sanitize_text_field(wp_unslash($_POST['rewrite_page_links'])) : ($opts['rewrite_page_links'] ?? '0');
2799 $opts['float_position'] = isset($_POST['position']) ? sanitize_text_field(wp_unslash($_POST['position'])) : ($opts['float_position'] ?? 'bottom-left');
2800 update_option('gptranslate_options', $opts);
2801 update_option('gptranslate_wizard_completed', 1, false);
2802 wp_send_json_success(['saved' => 1]);
2803 });
2804
2805 // ============================================================================
2806 // AJAX handler for getting already-translated URLs (crawler fast skip)
2807 // ============================================================================
2808 add_action('wp_ajax_gptranslate_get_translated_urls', function () {
2809 check_ajax_referer('gptranslate_crawler_nonce', 'nonce');
2810 if (!current_user_can('manage_options')) {
2811 wp_send_json_error(array('message' => 'Unauthorized'));
2812 return;
2813 }
2814 global $wpdb;
2815 $table = $wpdb->prefix . 'gptranslate';
2816
2817 // Get enabled target languages (excluding the default/source language)
2818 $opts = get_option('gptranslate_options', array());
2819 $defaultLanguage = strtolower($opts['language'] ?? 'en');
2820 $enabledLanguages = array_map('strtolower', $opts['languages'] ?? array());
2821 $targetLanguages = array_values(array_filter($enabledLanguages, function($lang) use ($defaultLanguage) {
2822 return $lang !== $defaultLanguage;
2823 }));
2824 $numTargetLanguages = count($targetLanguages);
2825
2826 if ($numTargetLanguages === 0) {
2827 wp_send_json_success(array('urls' => array()));
2828 return;
2829 }
2830
2831 // Fetch all published rows: pagelink + languagetranslated
2832 // pagelink already contains the language prefix (e.g. http://site/fr/page/)
2833 // so we normalize it in PHP to group by the original URL
2834 $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2835 "SELECT pagelink, languagetranslated FROM {$table} WHERE published = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2836 ARRAY_A
2837 );
2838
2839 // Helper: strip language prefix from URL path
2840 $stripLangPrefix = function($url) {
2841 $cleaned = rtrim($url, '/');
2842 $parsedUrl = parse_url($cleaned);
2843 if ($parsedUrl === false) {
2844 return $cleaned;
2845 }
2846 $pathname = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
2847 if (preg_match('#^/([a-z]{2}(-[a-z]{2})?)(?=/|$)#i', $pathname)) {
2848 $pathname = preg_replace('#^/[a-z]{2}(-[a-z]{2})?#i', '', $pathname);
2849 if (empty($pathname)) {
2850 $pathname = '/';
2851 }
2852 }
2853 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
2854 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
2855 $port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
2856 return $scheme . $host . $port . $pathname;
2857 };
2858
2859 // Group by normalized URL → collect translated languages
2860 $urlLangMap = array();
2861 foreach ($rows as $row) {
2862 $normalizedUrl = $stripLangPrefix($row['pagelink']);
2863 $lang = strtolower($row['languagetranslated']);
2864 if (!isset($urlLangMap[$normalizedUrl])) {
2865 $urlLangMap[$normalizedUrl] = array();
2866 }
2867 if (!in_array($lang, $urlLangMap[$normalizedUrl])) {
2868 $urlLangMap[$normalizedUrl][] = $lang;
2869 }
2870 }
2871
2872 // Keep only URLs where ALL target languages are present
2873 $fullyTranslatedUrls = array();
2874 foreach ($urlLangMap as $normalizedUrl => $translatedLangs) {
2875 $missing = array_diff($targetLanguages, $translatedLangs);
2876 if (empty($missing)) {
2877 $fullyTranslatedUrls[] = $normalizedUrl;
2878 }
2879 }
2880
2881 wp_send_json_success(array('urls' => $fullyTranslatedUrls));
2882 });
2883
2884 // ============================================================================
2885 // AJAX handler for testing API key validity
2886 // ============================================================================
2887 add_action('wp_ajax_gptranslate_test_apikey', function () {
2888 // Verify nonce
2889 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'gptranslate_test_apikey')) {
2890 wp_send_json_error(array('message' => 'Invalid security token'));
2891 return;
2892 }
2893
2894 // Verify admin permissions
2895 if (!current_user_can('manage_options')) {
2896 wp_send_json_error(array('message' => 'Unauthorized access'));
2897 return;
2898 }
2899
2900 $apiKey = isset($_POST['apikey']) ? sanitize_text_field(wp_unslash($_POST['apikey'])) : '';
2901 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
2902
2903 if (empty($apiKey)) {
2904 wp_send_json_error(array('message' => 'API key is empty'));
2905 return;
2906 }
2907
2908 $url = '';
2909 $headers = array('Content-Type' => 'application/json');
2910 $body = '';
2911
2912 try {
2913 if (strpos($model, 'gpt-') === 0 || strpos($model, 'grok-') === 0) {
2914 // OpenAI / ChatGPT / Grok
2915 $url = (strpos($model, 'grok-') === 0) ? 'https://api.x.ai/v1/chat/completions' : 'https://api.openai.com/v1/chat/completions';
2916 $headers['Authorization'] = 'Bearer ' . $apiKey;
2917 $useNewTokenParam = (strpos($model, 'gpt-4.1') === 0 || strpos($model, 'gpt-5') === 0);
2918 $tokenParam = $useNewTokenParam ? 'max_completion_tokens' : 'max_tokens';
2919 $body = wp_json_encode(array(
2920 'model' => $model,
2921 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2922 $tokenParam => 5
2923 ));
2924 } elseif (strpos($model, 'deepseek-') === 0) {
2925 // DeepSeek
2926 $url = 'https://api.deepseek.com/v1/chat/completions';
2927 $headers['Authorization'] = 'Bearer ' . $apiKey;
2928 $body = wp_json_encode(array(
2929 'model' => $model,
2930 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2931 'max_tokens' => 5
2932 ));
2933 } elseif (strpos($model, 'gemini-') === 0) {
2934 // Google Gemini
2935 $apiVersion = (strpos($model, '-preview') !== false) ? 'v1beta' : 'v1';
2936 $url = "https://generativelanguage.googleapis.com/{$apiVersion}/models/{$model}:generateContent";
2937 $headers['x-goog-api-key'] = $apiKey;
2938 $body = wp_json_encode(array(
2939 'contents' => array(array('parts' => array(array('text' => 'Hi')))),
2940 'generationConfig' => array('maxOutputTokens' => 5)
2941 ));
2942 } elseif (strpos($model, 'claude-') === 0) {
2943 // Claude / Anthropic
2944 $url = 'https://api.anthropic.com/v1/messages';
2945 $headers['x-api-key'] = $apiKey;
2946 $headers['anthropic-version'] = '2023-06-01';
2947 unset($headers['Authorization']);
2948 $body = wp_json_encode(array(
2949 'model' => $model,
2950 'messages' => array(array('role' => 'user', 'content' => 'Hi')),
2951 'max_tokens' => 5
2952 ));
2953 } elseif ($model === 'google-cloud-translation-api') {
2954 // Google Cloud Translation
2955 $url = 'https://translation.googleapis.com/language/translate/v2?key=' . urlencode($apiKey);
2956 $body = wp_json_encode(array(
2957 'q' => array('hello'),
2958 'source' => 'en',
2959 'target' => 'es',
2960 'format' => 'text'
2961 ));
2962 } elseif ($model === 'deepl-api') {
2963 // DeepL - Auto-detect endpoint based on API key type
2964 // Free API keys end with ':fx', paid keys don't have this suffix
2965 $deeplEndpoint = (strpos ( $apiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
2966 $url = $deeplEndpoint . '/v2/translate';
2967 $headers ['Authorization'] = 'DeepL-Auth-Key ' . $apiKey;
2968 $headers ['Content-Type'] = 'application/x-www-form-urlencoded';
2969 // DeepL requires repeated "text" params (not text[0])
2970 $body = 'text=' . urlencode ( 'hello' ) . '&source_lang=EN&target_lang=ES';
2971 } else {
2972 wp_send_json_error(array('message' => 'Unsupported model: ' . $model));
2973 return;
2974 }
2975
2976 $response = wp_remote_post($url, array(
2977 'headers' => $headers,
2978 'body' => $body,
2979 'timeout' => 60
2980 ));
2981
2982 if (is_wp_error($response)) {
2983 wp_send_json_error(array(
2984 'message' => $response->get_error_message(),
2985 'error_code' => 0,
2986 'http_code' => 0
2987 ));
2988 return;
2989 }
2990
2991 $httpCode = wp_remote_retrieve_response_code($response);
2992 $responseBody = wp_remote_retrieve_body($response);
2993
2994 if ($httpCode >= 200 && $httpCode < 300) {
2995 wp_send_json_success(array(
2996 'result' => true,
2997 'http_code' => $httpCode
2998 ));
2999 } else {
3000 $errorMessage = 'HTTP ' . $httpCode;
3001 $decoded = json_decode($responseBody, true);
3002 if ($decoded) {
3003 if (isset($decoded['error']['message'])) {
3004 $errorMessage = $decoded['error']['message'];
3005 } elseif (isset($decoded['error']['status'])) {
3006 $errorMessage = $decoded['error']['status'];
3007 }
3008 // Claude/Anthropic error format: {type: "error", error: {type, message}}
3009 if (isset($decoded['type']) && $decoded['type'] === 'error' && isset($decoded['error']['message'])) {
3010 $errorMessage = $decoded['error']['message'];
3011 }
3012 }
3013 wp_send_json_error(array(
3014 'message' => $errorMessage,
3015 'error_code' => $httpCode,
3016 'http_code' => $httpCode
3017 ));
3018 }
3019 } catch (Exception $e) {
3020 wp_send_json_error(array(
3021 'message' => $e->getMessage(),
3022 'error_code' => 0,
3023 'http_code' => 0
3024 ));
3025 }
3026 });
3027
3028 add_action('wp_ajax_gptranslate_bulk_delete', function () {
3029 if (!current_user_can('manage_options') || !check_ajax_referer('gptranslate_delete_translations', '_wpnonce', false)) {
3030 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3031 }
3032
3033 global $wpdb;
3034 $ids = isset($_POST['gptid']) ? array_map('intval', (array) $_POST['gptid']) : [];
3035
3036 if (empty($ids)) {
3037 wp_send_json_error('No records selected');
3038 }
3039
3040 $table = $wpdb->prefix . 'gptranslate';
3041 $in = implode(',', array_fill(0, count($ids), '%d'));
3042 $sql = "DELETE FROM {$table} WHERE id IN ($in)";
3043 $result = $wpdb->query($wpdb->prepare($sql, ...$ids)); // phpcs:ignore
3044
3045 if ($result === false) {
3046 wp_send_json_error('Database error');
3047 }
3048
3049 wp_send_json_success();
3050 });
3051
3052 // Handle Export CSV
3053 add_action('admin_post_gptranslate_export_translations_csv', function () {
3054 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_csv', 'gptranslate_export_csv_nonce')) {
3055 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3056 }
3057
3058 global $wpdb;
3059 $table = $wpdb->prefix . 'gptranslate';
3060
3061 $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
3062
3063 if (!$records) {
3064 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
3065 }
3066
3067 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3068 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
3069
3070 header('Content-Type: text/csv');
3071 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.csv"');
3072
3073 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
3074 $output = fopen('php://output', 'w');
3075
3076 // Intestazioni CSV
3077 fputcsv($output, array_keys($records[0]), ",", '"', "\\");
3078
3079 foreach ($records as $record) {
3080 fputcsv($output, $record, ",", '"', "\\");
3081 }
3082
3083 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
3084 fclose($output);
3085 exit;
3086 });
3087
3088 // Handle Import CSV
3089 add_action('admin_post_gptranslate_import_translations_csv', function () {
3090 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_csv', 'gptranslate_import_csv_nonce')) {
3091 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3092 }
3093
3094 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
3095 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
3096 }
3097
3098 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3099 if (!is_uploaded_file($tmp_name)) {
3100 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3101 }
3102
3103 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
3104 $file = fopen($tmp_name, 'r');
3105 if (!$file) {
3106 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3107 }
3108
3109 $headers = fgetcsv($file);
3110 if (!$headers) {
3111 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_CSV_FORMAT')));
3112 }
3113
3114 global $wpdb;
3115 $table = $wpdb->prefix . 'gptranslate';
3116
3117 while (($row = fgetcsv($file)) !== false) {
3118 $countHeaders = count($headers);
3119 $countRow = count($row);
3120 // Invalid combine
3121 if($countHeaders != $countRow) {
3122 continue;
3123 }
3124 $record = array_combine($headers, $row);
3125 if (empty($record['pagelink'])) {
3126 continue; // skip if no primary key
3127 }
3128
3129 $pagelink = sanitize_text_field($record['pagelink']);
3130 $exists = $wpdb->get_var($wpdb->prepare(
3131 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
3132 $pagelink, $record['languageoriginal'], $record['languagetranslated']
3133 ));// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3134
3135 $data = [
3136 'translated_alias' => $record['translated_alias'],
3137 'translations' => $record['translations'],
3138 'alt_translations' => $record['alt_translations'],
3139 'languageoriginal' => sanitize_text_field($record['languageoriginal']),
3140 'languagetranslated' => sanitize_text_field($record['languagetranslated']),
3141 'published' => isset($record['published']) ? (int)$record['published'] : 1,
3142 'translate_date' => $record['translate_date'],
3143 'translation_engine' => sanitize_text_field($record['translation_engine']),
3144 ];
3145
3146 if ($exists) {
3147 $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3148 $table,
3149 $data,
3150 ['pagelink' => $pagelink, 'languageoriginal' => sanitize_text_field($record['languageoriginal']), 'languagetranslated' => sanitize_text_field($record['languagetranslated'])],
3151 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s'],
3152 ['%s','%s','%s']
3153 );
3154 } else {
3155 $data['pagelink'] = $pagelink;
3156 $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3157 $table,
3158 $data,
3159 ['%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
3160 );
3161 }
3162 }
3163
3164 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
3165 fclose($file);
3166
3167 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
3168 exit;
3169 });
3170
3171 // Handle Export XLIFF
3172 add_action('admin_post_gptranslate_export_translations_xliff', function () {
3173 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_export_xliff', 'gptranslate_export_xliff_nonce')) {
3174 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3175 }
3176
3177 global $wpdb;
3178 $table = $wpdb->prefix . 'gptranslate';
3179
3180 $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
3181
3182 if (!$records) {
3183 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_NOTRANSLATIONS')));
3184 }
3185
3186 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3187 $fileDate = (date_i18n('Y-m-d', strtotime($localDate)));
3188
3189 header('Content-Type: application/xml; charset=utf-8');
3190 header('Content-Disposition: attachment; filename="gptranslate-translations-' . $fileDate . '.xliff"');
3191
3192 $xml = new SimpleXMLElement('<xliff/>');
3193 $xml->addAttribute('version', '1.2');
3194
3195 foreach ($records as $record) {
3196 $file = $xml->addChild('file');
3197 $file->addAttribute('source-language', $record['languageoriginal']);
3198 $file->addAttribute('target-language', $record['languagetranslated']);
3199 $file->addAttribute('datatype', 'html');
3200 $file->addAttribute('original', $record['pagelink']);
3201
3202 $body = $file->addChild('body');
3203
3204 $translations = json_decode($record['translations'], true) ?: [];
3205 foreach ($translations as $source => $target) {
3206 $unit = $body->addChild('trans-unit');
3207 $unit->addAttribute('id', md5($source));
3208 $unit->addChild('source', htmlspecialchars($source));
3209 $unit->addChild('target', htmlspecialchars($target));
3210 }
3211 }
3212
3213 echo $xml->asXML();
3214 exit;
3215 });
3216
3217 // Handle Import XLIFF
3218 add_action('admin_post_gptranslate_import_translations_xliff', function () {
3219 if (!current_user_can('manage_options') || !check_admin_referer('gptranslate_import_xliff', 'gptranslate_import_xliff_nonce')) {
3220 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3221 }
3222
3223 if (!isset($_FILES['import_file'], $_FILES['import_file']['error'], $_FILES['import_file']['tmp_name']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
3224 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')));
3225 }
3226
3227 $tmp_name = $_FILES['import_file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3228 if (!is_uploaded_file($tmp_name)) {
3229 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FAILED_UPLOADED_FILE')));
3230 }
3231
3232 $xml = simplexml_load_file($tmp_name);
3233 if (!$xml) {
3234 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_INVALID_XLIFF_FORMAT')));
3235 }
3236
3237 global $wpdb;
3238 $table = $wpdb->prefix . 'gptranslate';
3239
3240 foreach ($xml->file as $file) {
3241 $sourceLang = (string)$file['source-language'];
3242 $targetLang = (string)$file['target-language'];
3243 $pagelink = (string)$file['original'];
3244
3245 $translations = [];
3246 foreach ($file->body->{'trans-unit'} as $unit) {
3247 $src = (string)$unit->source;
3248 $tgt = (string)$unit->target;
3249 $translations[$src] = $tgt;
3250 }
3251
3252 $json_translations = wp_json_encode($translations);
3253
3254 $exists = $wpdb->get_var($wpdb->prepare(
3255 "SELECT id FROM $table WHERE pagelink = %s AND languageoriginal = %s AND languagetranslated = %s",
3256 $pagelink, $sourceLang, $targetLang
3257 )); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
3258
3259 if ($exists) {
3260 // �
3261 Update only the translations and date, keep everything else intact
3262 $wpdb->update(
3263 $table,
3264 [
3265 'translations' => $json_translations,
3266 'translate_date' => current_time('mysql'),
3267 ],
3268 ['id' => $exists]
3269 ); // phpcs:ignore
3270 } else {
3271 // �
3272 Insert full record only if it doesn't exist
3273 $data = [
3274 'pagelink' => $pagelink,
3275 'translated_alias' => '',
3276 'translations' => $json_translations,
3277 'alt_translations' => '[]',
3278 'languageoriginal' => $sourceLang,
3279 'languagetranslated'=> $targetLang,
3280 'published' => 1,
3281 'translate_date' => current_time('mysql'),
3282 'translation_engine'=> 'chatgpt',
3283 ];
3284 $wpdb->insert($table, $data); // phpcs:ignore
3285 }
3286 }
3287
3288 wp_redirect(admin_url('admin.php?page=gptranslate&imported=1'));
3289 exit;
3290 });
3291
3292 // Handle Export XML Sitemap
3293 add_action('admin_post_gptranslate_export_xml_sitemap', function () {
3294 if (!current_user_can('manage_options') || !isset($_POST['gptranslate_export_xml_sitemap']) || $_POST['gptranslate_export_xml_sitemap'] !== 'b62d18a19b') {
3295 wp_die(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3296 }
3297
3298 global $wpdb;
3299 $table = $wpdb->prefix . 'gptranslate';
3300
3301 // Get language filter from POST parameter
3302 $language_filter = isset($_POST['sitemap_language']) ? sanitize_text_field(wp_unslash($_POST['sitemap_language'])) : '';
3303
3304 // Solo record con translated_alias non vuoto
3305 $query = "
3306 SELECT *
3307 FROM $table
3308 WHERE translated_alias IS NOT NULL
3309 AND translated_alias != ''";
3310
3311 // Add language filter if specified and not "all"
3312 if (!empty($language_filter) && $language_filter !== 'all') {
3313 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
3314 }
3315
3316 $query .= " ORDER BY translate_date DESC";
3317
3318 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3319
3320 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3321 $fileDate = date_i18n('Y-m-d', strtotime($localDate));
3322
3323 // Add language suffix to filename if specific language is selected
3324 if (!empty($language_filter) && $language_filter !== 'all') {
3325 $fileDate .= '-' . sanitize_file_name($language_filter);
3326 }
3327
3328 $dom = new DOMDocument('1.0', 'UTF-8');
3329 $dom->preserveWhiteSpace = false;
3330 $dom->formatOutput = true;
3331
3332 $urlset = $dom->createElement('urlset');
3333 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
3334 $dom->appendChild($urlset);
3335
3336 if (empty($records)) {
3337 $urlset->appendChild($dom->createTextNode(''));
3338 }
3339
3340 foreach ($records as $record) {
3341 $url = $dom->createElement('url');
3342 $loc = $dom->createElement('loc');
3343 $loc->appendChild($dom->createTextNode($record['translated_alias']));
3344 $url->appendChild($loc);
3345
3346 if (!empty($record['translate_date'])) {
3347 $lastmod = $dom->createElement('lastmod', date('c', strtotime($record['translate_date'])));
3348 $url->appendChild($lastmod);
3349 }
3350
3351 $urlset->appendChild($url);
3352 }
3353
3354 header('Content-Type: application/xml; charset=UTF-8');
3355 header('Content-Disposition: attachment; filename="gptranslate-sitemap-' . $fileDate . '.xml"');
3356 echo $dom->saveXML();
3357 exit;
3358 });
3359
3360 // Register Ajax handler
3361 add_action('wp_ajax_gptranslate_migrate_translations', function() {
3362 if (!current_user_can('manage_options')) {
3363 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')));
3364 }
3365
3366 check_ajax_referer('gptranslate_migrate_translations');
3367
3368 global $wpdb;
3369 $table = $wpdb->prefix . 'gptranslate';
3370 $old = isset($_POST['old_domain']) ? sanitize_text_field(wp_unslash($_POST['old_domain'])) : '';
3371 $new = isset($_POST['new_domain']) ? sanitize_text_field(wp_unslash($_POST['new_domain'])) : '';
3372
3373 if (empty($old) || empty($new)) {
3374 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_MISSING_DOMAIN_VALUES')));
3375 }
3376
3377 $query = $wpdb->prepare(
3378 "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
3379 $old, $new, $old, $new
3380 );
3381
3382 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
3383 $result = $wpdb->query($query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3384
3385 if ($result === false) {
3386 wp_send_json_error(esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_DATABASE_ERROR')));
3387 } else {
3388 wp_send_json_success();
3389 }
3390 });
3391
3392 function gptranslate_export_settings() {
3393 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_export_settings', 'gptranslate_export_settings_nonce')) {
3394 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
3395 }
3396
3397 $options = get_option( 'gptranslate_options', [] );
3398
3399 $localDate = get_date_from_gmt(gmdate('Y-m-d'));
3400 $fileDate = ( date_i18n('Y-m-d', strtotime($localDate)) );
3401
3402 header( 'Content-Type: application/json' );
3403 header( 'Content-Disposition: attachment; filename="gptranslate-settings-' . $fileDate . '.json"' );
3404 echo wp_json_encode( $options );
3405 exit;
3406 }
3407 add_action( 'admin_post_gptranslate_export_settings', 'gptranslate_export_settings' );
3408
3409 function gptranslate_import_settings() {
3410 if ( ! current_user_can( 'manage_options' ) || !check_admin_referer('gptranslate_import_settings', 'gptranslate_import_settings_nonce')) {
3411 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UNAUTHORIZED_REQUEST')) );
3412 }
3413
3414 if ( empty( $_FILES['gptranslate_settings_file']['tmp_name'] ) ) {
3415 wp_die( esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_UPLOAD_FAILED')) );
3416 }
3417
3418 $content = file_get_contents( $_FILES['gptranslate_settings_file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3419 $decoded = json_decode( $content, true );
3420
3421 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
3422 wp_die( esc_html(GPTranslate::loadTranslations('PLG_GPTRANSLATE_INVALID_JSON_TRANSLATIONS') ) );
3423 }
3424
3425 // Optional: sanitize known values, or just update if you're confident of source
3426 update_option( 'gptranslate_options', $decoded );
3427
3428 wp_safe_redirect( admin_url( 'admin.php?page=gptranslate-settings&settingsimported=1' ) );
3429 exit;
3430 }
3431 add_action( 'admin_post_gptranslate_import_settings', 'gptranslate_import_settings' );
3432
3433 // ============================================================================
3434 // Shortcode [gptranslate] - Renders the language switcher at the shortcode position
3435 // ============================================================================
3436 add_shortcode('gptranslate', function ($atts) {
3437 $settings = get_option("gptranslate_options");
3438
3439 // Disable interface
3440 if (!empty($settings['disable_control'])) {
3441 return '';
3442 }
3443
3444 $wrapper_class = 'gptranslate_wrapper';
3445 $custom_selector = $settings['wrapper_selector'] ?? '.gptranslate_wrapper';
3446
3447 // If the user has set a custom wrapper selector (not the default), use that class
3448 if ($custom_selector !== '.gptranslate_wrapper' && strpos($custom_selector, '.') === 0) {
3449 $wrapper_class = substr($custom_selector, 1);
3450 }
3451
3452 // Flag that the shortcode was used, to prevent duplicate wrapper in footer
3453 if (!defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
3454 define('GPTRANSLATE_SHORTCODE_RENDERED', true);
3455 }
3456
3457 return '<div class="' . esc_attr($wrapper_class) . '"></div>';
3458 });
3459
3460 add_action('wp_footer', function () {
3461 // Add the default target container if default CSS selector
3462 $settings = get_option("gptranslate_options");
3463
3464 // Disable interface
3465 if($settings ['disable_control']) {
3466 $settings ['wrapper_selector'] = '';
3467 }
3468
3469 // Skip the automatic footer wrapper if shortcode was already used
3470 if (defined('GPTRANSLATE_SHORTCODE_RENDERED')) {
3471 return;
3472 }
3473
3474 if ($settings ['wrapper_selector'] == '.gptranslate_wrapper') {
3475 echo '<div class="gptranslate_wrapper" id="gpt-wrapper"></div>';
3476 }
3477 });
3478
3479 // Get available (enabled) languages for sitemap filtering (excluding original language)
3480 function gptranslate_get_available_languages() {
3481 $opts = get_option('gptranslate_options', array());
3482
3483 // Get enabled target languages from settings
3484 $enabled_languages = isset($opts['languages']) ? array_map('strtolower', (array)$opts['languages']) : array();
3485
3486 // Exclude the original/source language
3487 $original_language = isset($opts['language']) ? strtolower($opts['language']) : '';
3488 $enabled_languages = array_filter($enabled_languages, function($lang) use ($original_language) {
3489 return $lang !== $original_language;
3490 });
3491
3492 return $enabled_languages ? array_values($enabled_languages) : array();
3493 }
3494
3495 // ============================================================================
3496 // AUTO-CRAWL ON POST PUBLISH
3497 // Automatically crawl new/updated posts when they are published
3498 // ============================================================================
3499
3500 /**
3501 * Hook triggered when a post is published/saved.
3502 * Saves the post URL to a transient queue if auto-crawl is enabled.
3503 */
3504 add_action('wp_after_insert_post', function($post_id, $post, $update, $post_before) {
3505 // Check if auto-crawl is enabled in settings
3506 $settings = get_option('gptranslate_options', []);
3507 // Handle both string '1' and integer 1 for the setting value
3508 $auto_crawl_enabled = isset($settings['auto_crawl_on_publish']) && ($settings['auto_crawl_on_publish'] == 1 || $settings['auto_crawl_on_publish'] === '1');
3509
3510 if (!$auto_crawl_enabled) {
3511 return;
3512 }
3513
3514 // Only trigger auto-crawl when post transitions TO published status (first publish only)
3515 $was_published = $post_before && $post_before->post_status === 'publish';
3516 $is_now_published = $post->post_status === 'publish';
3517
3518 // Must be newly published (not already published before)
3519 if (!($is_now_published && !$was_published)) {
3520 return;
3521 }
3522
3523 // Skip post types that shouldn't be translated (attachments, revisions, etc.)
3524 $exclude_types = ['attachment', 'revision', 'nav_menu_item'];
3525 if (in_array($post->post_type, $exclude_types, true)) {
3526 return;
3527 }
3528
3529 // Skip if this is an auto-draft
3530 if ($post->post_type === 'post' || $post->post_type === 'page') {
3531 // Get the post URL
3532 $post_url = get_permalink($post_id);
3533 if (empty($post_url)) {
3534 return;
3535 }
3536
3537 // Add language prefix to URL if needed (prevents 301 redirect that strips UTF-8 chars)
3538 $settings = get_option('gptranslate_options', []);
3539 if (!empty($settings['rewrite_language_url']) && empty($settings['omit_prefix_original_language'])) {
3540 $default_lang = $settings['language'] ?? 'en';
3541
3542 if (!empty($settings['subfolder_installation'])) {
3543 // Subfolder installation: add language prefix after subfolder
3544 // e.g., http://site.com/subfolder/page → http://site.com/subfolder/en/page
3545 $site_url = trailingslashit(get_site_url());
3546 $relative_path = str_replace($site_url, '', $post_url);
3547 $post_url = $site_url . $default_lang . '/' . ltrim($relative_path, '/');
3548 } else {
3549 // Root installation: add language prefix after domain
3550 // e.g., http://site.com/page → http://site.com/en/page
3551 $post_url = preg_replace(
3552 '#^(https?://[^/]+)/#',
3553 '$1/' . $default_lang . '/',
3554 $post_url
3555 );
3556 }
3557 }
3558
3559 // Save the URL to transient queue (per-user, 5 minute TTL)
3560 $queue_key = 'gpt_auto_crawl_queue_' . get_current_user_id();
3561 set_transient($queue_key, [
3562 'url' => $post_url,
3563 'post_id' => $post_id,
3564 'timestamp' => time()
3565 ], 5 * MINUTE_IN_SECONDS);
3566 }
3567 }, 10, 4);
3568
3569 /**
3570 * AJAX handler to clear the auto-crawl queue.
3571 * Called by auto-crawler.js after the post is published and user navigates to admin.
3572 * This prevents re-triggering the crawl on page reload.
3573 */
3574 add_action('wp_ajax_gpt_clear_auto_crawl_queue', function() {
3575 // Verify nonce for security
3576 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'gpt_auto_crawl_nonce')) {
3577 wp_send_json_error(['message' => 'Invalid nonce']);
3578 wp_die();
3579 }
3580
3581 // Delete the transient queue for current user
3582 $queue_key = 'gpt_auto_crawl_queue_' . get_current_user_id();
3583 delete_transient($queue_key);
3584
3585 wp_send_json_success();
3586 wp_die();
3587 });
3588
3589 // POST API REST Translations storage
3590 add_action('rest_api_init', function () {
3591 register_rest_route('gptranslate/v1', '/request', [
3592 'methods' => 'POST',
3593 'callback' => 'gpt_handle_request',
3594 'permission_callback' => 'gptranslate_public_permission'
3595 ]);
3596 });
3597
3598 // Real-time XML Sitemap endpoint - public, generates sitemap on-the-fly
3599 add_action('rest_api_init', function () {
3600 register_rest_route('gptranslate/v1', '/sitemap.xml', [
3601 'methods' => 'GET',
3602 'callback' => 'gptranslate_realtime_sitemap',
3603 'permission_callback' => '__return_true'
3604 ]);
3605 });
3606
3607 function gptranslate_realtime_sitemap() {
3608 global $wpdb;
3609 $table = $wpdb->prefix . 'gptranslate';
3610
3611 // Get language filter from query parameter
3612 $language_filter = isset($_GET['language']) ? sanitize_text_field(wp_unslash($_GET['language'])) : '';
3613
3614 $query = "
3615 SELECT translated_alias, translate_date
3616 FROM $table
3617 WHERE translated_alias IS NOT NULL
3618 AND translated_alias != ''";
3619
3620 // Add language filter if specified and not "all"
3621 if (!empty($language_filter) && $language_filter !== 'all') {
3622 $query .= " AND languagetranslated = '" . esc_sql($language_filter) . "'";
3623 }
3624
3625 $query .= " ORDER BY translate_date DESC";
3626
3627 $records = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3628
3629 $dom = new DOMDocument('1.0', 'UTF-8');
3630 $dom->preserveWhiteSpace = false;
3631 $dom->formatOutput = true;
3632
3633 $urlset = $dom->createElement('urlset');
3634 $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
3635 $dom->appendChild($urlset);
3636
3637 if (empty($records)) {
3638 $urlset->appendChild($dom->createTextNode(''));
3639 }
3640
3641 foreach ($records as $record) {
3642 $url = $dom->createElement('url');
3643 $loc = $dom->createElement('loc');
3644 $loc->appendChild($dom->createTextNode($record['translated_alias']));
3645 $url->appendChild($loc);
3646
3647 if (!empty($record['translate_date'])) {
3648 $lastmod = $dom->createElement('lastmod', gmdate('c', strtotime($record['translate_date'])));
3649 $url->appendChild($lastmod);
3650 }
3651
3652 $urlset->appendChild($url);
3653 }
3654
3655 header('Content-Type: application/xml; charset=UTF-8');
3656 echo $dom->saveXML();
3657 exit;
3658 }
3659
3660 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
3661 // Remove the default 'Settings' item
3662 unset($links[0]);
3663
3664 $wizard_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-wizard')) . '">Wizard</a>';
3665 array_unshift($links, $wizard_link);
3666
3667 $settings_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate-settings')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_SETTINGS_MENU_TITLE')) . '</a>';
3668 array_unshift($links, $settings_link);
3669
3670 $translations_link = '<a href="' . esc_url(admin_url('admin.php?page=gptranslate')) . '">' . esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_TRANSLATIONS')) . '</a>';
3671 array_unshift($links, $translations_link);
3672
3673 return $links;
3674 });
3675
3676 /**
3677 * Get (or lazily create) the cryptographically random API secret.
3678 *
3679 * Replaces the predictable hash('sha256', get_site_url()) shared secret.
3680 * The value is generated once with wp_generate_password() (which uses
3681 * a cryptographically secure RNG) and persisted in wp_options.
3682 *
3683 * @return string
3684 */
3685 function gptranslate_get_api_secret() {
3686 $secret = get_option( 'gptranslate_api_secret' );
3687 if ( empty( $secret ) || ! is_string( $secret ) || strlen( $secret ) < 32 ) {
3688 $secret = wp_generate_password( 64, false, false );
3689 update_option( 'gptranslate_api_secret', $secret, false );
3690 }
3691 return $secret;
3692 }
3693
3694 /**
3695 * Permission callback public API
3696 * @param WP_REST_Request $request
3697 * @return bool|WP_Error
3698 */
3699 function gptranslate_public_permission( WP_REST_Request $request ) {
3700 // 1) Random API secret sent via header (constant-time comparison)
3701 $headerApiKey = (string) $request->get_header('x-gptranslate-key');
3702 $restApiKey = gptranslate_get_api_secret();
3703 if ( ! hash_equals( $restApiKey, $headerApiKey ) ) {
3704 return new WP_Error( 'rest_forbidden', esc_html(GPTranslate::loadTranslation('PLG_GPTRANSLATE_FORBIDDEN_APIKEY')), [ 'status' => 403 ] );
3705 }
3706
3707 // 2) Nonce verification for write operations (CSRF protection)
3708 $params = $request->get_json_params();
3709 if ( empty( $params ) ) {
3710 $params = $request->get_body_params();
3711 }
3712 $task = isset( $params['task'] ) ? sanitize_text_field( $params['task'] ) : '';
3713 $writeTasks = array( 'storetranslations', 'syncTranslation' );
3714 if ( in_array( $task, $writeTasks, true ) ) {
3715 $nonce = $request->get_header('X-WP-Nonce');
3716 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
3717 return new WP_Error( 'rest_forbidden_nonce', 'Invalid or expired security token (nonce).', [ 'status' => 403 ] );
3718 }
3719 }
3720
3721 // 3) Admin-only tasks: require manage_options capability
3722 $adminTasks = array( 'syncTranslation' );
3723 if ( in_array( $task, $adminTasks, true ) && ! current_user_can( 'manage_options' ) ) {
3724 return new WP_Error( 'rest_forbidden_admin', 'Admin access required.', [ 'status' => 403 ] );
3725 }
3726
3727 return true;
3728 }
3729
3730 /**
3731 * Normalize WP URL
3732 * @param string $url
3733 * @return string
3734 */
3735 function gpt_trailingslashit_url($url) {
3736 $parsed = wp_parse_url($url);
3737 if (empty($parsed['path'])) {
3738 $parsed['path'] = '/';
3739 } else {
3740 $parsed['path'] = trailingslashit(untrailingslashit($parsed['path']));
3741 }
3742
3743 $rebuilt = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : '';
3744 $rebuilt .= $parsed['host'] ?? '';
3745 $rebuilt .= $parsed['path'];
3746 if (!empty($parsed['query'])) {
3747 $rebuilt .= '?' . $parsed['query'];
3748 }
3749 if (!empty($parsed['fragment'])) {
3750 $rebuilt .= '#' . $parsed['fragment'];
3751 }
3752
3753 return $rebuilt;
3754 }
3755
3756 /**
3757 * Callback per GET/STORE translations via REST.
3758 * Frontend API
3759 *
3760 * @param WP_REST_Request $request
3761 * @return WP_REST_Response
3762 */
3763 function gpt_handle_request( WP_REST_Request $request ) {
3764 global $wpdb;
3765
3766 $table = $wpdb->prefix . 'gptranslate';
3767
3768 $params = $request->get_json_params();
3769
3770 if(!$params) {
3771 $params = $request->get_body_params();
3772 }
3773
3774 // Sanitize input params
3775 $task = sanitize_text_field( $params['task'] ?? '' );
3776 $pageLink = esc_url_raw( $params['pagelink'] ?? '' );
3777 $translatedAlias = esc_url_raw( $params['translated_alias'] ?? '' );
3778 $languageOriginal = sanitize_text_field( $params['language_original'] ?? '' );
3779 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
3780 $translationEngine = sanitize_text_field( $params['translation_engine'] ?? '' );
3781 $retriggerTranslation = (int) sanitize_text_field( $params['retrigger'] ?? false );
3782 $isIncremental = (int) sanitize_text_field( $params['incremental'] ?? 0 );
3783
3784 $now = current_time( 'mysql' );
3785
3786 $response = [ 'result' => false ];
3787
3788 if ( $task === 'storetranslations' ) {
3789 // Ensure there is not a mismatching insert with the same languages
3790 if($languageOriginal == $languageTranslated) {
3791 $response['result'] = true;
3792 return rest_ensure_response( $response );
3793 }
3794
3795 // Fetch raw param (could be array or JSON string)
3796 $rawFull = $params['translations'] ?? '[]';
3797 $rawAlt = $params['alt_translations'] ?? '[]';
3798
3799 // If it’s already a string (JSON), use it directly.
3800 // If it’s an array (unlikely with FormData), JSON encode it.
3801 if ( is_string( $rawFull ) && json_decode( $rawFull ) !== null ) {
3802 $fullTranslations = $rawFull;
3803 } else {
3804 $fullTranslations = wp_json_encode( (array) $rawFull );
3805 }
3806
3807 if ( is_string( $rawAlt ) && json_decode( $rawAlt ) !== null ) {
3808 $altTranslations = $rawAlt;
3809 } else {
3810 $altTranslations = wp_json_encode( (array) $rawAlt );
3811 }
3812
3813 // Check if record already exists
3814 $existing = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3815 "SELECT id FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3816 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3817 "\n AND languageoriginal = %s" .
3818 "\n AND languagetranslated = %s",
3819 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3820 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic query built with placeholders, safely prepared
3821
3822 $existing_id = $existing ? (int)$existing : null;
3823 $opts = get_option('gptranslate_options', []);
3824
3825 // Only if it is a retrigger then ignore the db processing
3826 if($retriggerTranslation !== 1) {
3827 if ( $existing_id ) {
3828 if ( $isIncremental ) {
3829 // Incremental update: ALWAYS merge only new/modified translations (safe - never touch alt_translations or translated_alias)
3830 $existing_record = $wpdb->get_row( $wpdb->prepare(
3831 "SELECT translations FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
3832 " WHERE id = %d",
3833 (int) $existing_id
3834 ), ARRAY_A );
3835
3836 if ( $existing_record ) {
3837 // Merge translations: respect lock_translations setting
3838 $existing_trans = json_decode( $existing_record['translations'], true ) ?: [];
3839 $new_trans = json_decode( $fullTranslations, true ) ?: [];
3840
3841 // If lock_translations enabled: preserve existing (even more conservative on incremental)
3842 // If lock_translations disabled: allow updates to existing translations
3843 if ( !empty($opts['lock_translations']) ) {
3844 $merged_trans = array_merge( $new_trans, $existing_trans ); // existing wins
3845 } else {
3846 $merged_trans = array_merge( $existing_trans, $new_trans ); // new wins (updates)
3847 }
3848
3849 $update_data = [
3850 'translations' => wp_json_encode( $merged_trans ),
3851 'translate_date' => $now,
3852 'translation_engine' => $translationEngine,
3853 // Never touch alt_translations or translated_alias on incremental updates
3854 ];
3855
3856 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3857 $table,
3858 $update_data,
3859 [ 'id' => (int) $existing_id ],
3860 [ '%s', '%s', '%s' ],
3861 [ '%d' ]
3862 );
3863 $response['result'] = ( $updated !== false );
3864 } else {
3865 $response['result'] = false;
3866 }
3867 } else {
3868 // Full (non-incremental) update: protected by lock_translations setting
3869 if ( !empty($opts['lock_translations']) ) {
3870 // lock_translations enabled: skip full UPDATE
3871 $response['result'] = false;
3872 } else {
3873 // lock_translations disabled: overwrite everything (2.29 behavior)
3874 $updated = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3875 $table,
3876 [
3877 'translations' => $fullTranslations,
3878 'alt_translations' => $altTranslations,
3879 'translated_alias' => $translatedAlias,
3880 'translate_date' => $now,
3881 'translation_engine' => $translationEngine,
3882 ],
3883 [ 'id' => (int) $existing_id ],
3884 [ '%s', '%s', '%s', '%s', '%s' ],
3885 [ '%d' ]
3886 );
3887 $response['result'] = ( $updated !== false );
3888 }
3889 }
3890 } else {
3891 // INSERT
3892 $insert_data = [
3893 'pagelink' => $pageLink,
3894 'translations' => $fullTranslations,
3895 'alt_translations' => $altTranslations,
3896 'translated_alias' => $translatedAlias,
3897 'languageoriginal' => $languageOriginal,
3898 'languagetranslated' => $languageTranslated,
3899 'published' => 1,
3900 'translate_date' => $now,
3901 'translation_engine' => $translationEngine,
3902 ];
3903
3904 $inserted = $wpdb->insert( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3905 $table,
3906 $insert_data,
3907 [ '%s','%s','%s','%s','%s','%s','%d','%s','%s' ]
3908 );
3909 $response['result'] = ( $inserted !== false );
3910 }
3911 } else {
3912 $response['result'] = true;
3913 }
3914 } elseif ($task === 'gettranslations') {
3915 $opts = get_option('gptranslate_options', []);
3916 if (!empty($opts['realtime_translations']) || $retriggerTranslation === 1) {
3917 $response['result'] = false;
3918 } else {
3919 // Prepare decoded version for URL matching
3920 $pageLinkDecoded = urldecode($pageLink);
3921
3922 if ($opts['rewrite_language_url'] == 1 && $opts['rewrite_language_alias'] == 1) {
3923 // Check 8 variants (with/without slash + encoded/decoded)
3924 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3925 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3926 "\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)" .
3927 "\n AND languageoriginal = %s" .
3928 "\n AND languagetranslated = %s" .
3929 "\n AND published = 1",
3930 rtrim($pageLink, '/'),
3931 rtrim($pageLink, '/') . '/',
3932 rtrim($pageLinkDecoded, '/'),
3933 rtrim($pageLinkDecoded, '/') . '/',
3934 rtrim($pageLink, '/'),
3935 rtrim($pageLink, '/') . '/',
3936 rtrim($pageLinkDecoded, '/'),
3937 rtrim($pageLinkDecoded, '/') . '/',
3938 $languageOriginal,
3939 $languageTranslated
3940 ), ARRAY_A);
3941 } else {
3942 // Check 4 variants (with/without slash + encoded/decoded)
3943 $row = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3944 "SELECT translations, alt_translations, translated_alias, pagelink FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
3945 "\n WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s)" .
3946 "\n AND languageoriginal = %s" .
3947 "\n AND languagetranslated = %s" .
3948 "\n AND published = 1",
3949 rtrim($pageLink, '/'),
3950 rtrim($pageLink, '/') . '/',
3951 rtrim($pageLinkDecoded, '/'),
3952 rtrim($pageLinkDecoded, '/') . '/',
3953 $languageOriginal,
3954 $languageTranslated
3955 ), ARRAY_A);
3956 }
3957
3958 if ($row) {
3959 $response['result'] = true;
3960 $response['translations'] = json_decode($row['translations'], true) ?: [];
3961 $response['alt_translations'] = json_decode($row['alt_translations'], true) ?: [];
3962 $response['translated_alias'] = $row['translated_alias'];
3963 $response['pagelink_alias'] = $row['pagelink'];
3964 } else {
3965 $response['result'] = false;
3966 }
3967 }
3968 } elseif ($task == 'getaliastranslation') {
3969 // Always perform a new realtime translation if the option is enabled
3970 try {
3971 $row = $wpdb->get_row( $wpdb->prepare(
3972 "SELECT translated_alias FROM {$table}" .
3973 "\n WHERE (pagelink = %s OR pagelink = %s)" .
3974 "\n AND languageoriginal = %s" .
3975 "\n AND languagetranslated = %s" .
3976 "\n AND published = 1",
3977 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $languageOriginal, $languageTranslated
3978 ), ARRAY_A );
3979
3980 if ( $row ) {
3981 $response['result'] = true;
3982 $response['translated_alias'] = $row['translated_alias'] ?? '';
3983 } else {
3984 $response['result'] = false;
3985 }
3986 } catch ( Exception $e ) {
3987 $response['result'] = false;
3988 $response['exception'] = $e->getMessage();
3989 }
3990 } elseif ( $task === 'syncTranslation' ) {
3991 $original = wp_unslash( $params['original'] ?? '' );
3992 $translated = wp_unslash( $params['translated'] ?? '' );
3993 $languageTranslated = sanitize_text_field( $params['language_translated'] ?? '' );
3994 $requestedType = sanitize_text_field( $params['translation_type'] ?? 'translations' );
3995 $translationType = in_array( $requestedType, array( 'translations', 'alt_translations' ), true ) ? $requestedType : 'translations';
3996
3997 // Recupera tutti i record nella lingua target
3998 $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
3999 "SELECT id, {$translationType}, languagetranslated" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
4000 "\n FROM {$table}" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic query built with placeholders, safely prepared
4001 "\n WHERE languagetranslated = %s",
4002 $languageTranslated
4003 ) );
4004
4005 $updatedCount = 0;
4006
4007 foreach ( $rows as $row ) {
4008 $currentTranslations = json_decode( $row->$translationType, true );
4009
4010 if ( is_array( $currentTranslations ) && array_key_exists( $original, $currentTranslations ) ) {
4011 // Aggiorna la traduzione e salva
4012 $currentTranslations[ $original ] = $translated;
4013 $jsonUpdated = wp_json_encode( $currentTranslations );
4014
4015 $success = $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
4016 $table,
4017 [ $translationType => $jsonUpdated, 'translate_date' => $now ],
4018 [ 'id' => $row->id ],
4019 [ '%s', '%s' ],
4020 [ '%d' ]
4021 );
4022
4023 if ( $success !== false ) {
4024 $updatedCount++;
4025 }
4026 }
4027 }
4028
4029 $response['result'] = $updatedCount > 0;
4030 } elseif ($task == 'gettranslatedaliases') {
4031 try {
4032 if ($languageTranslated) {
4033 $rows = $wpdb->get_results(
4034 $wpdb->prepare(
4035 "SELECT pagelink, translated_alias
4036 FROM {$table}
4037 WHERE languagetranslated = %s
4038 AND published = 1",
4039 $languageTranslated
4040 ),
4041 ARRAY_A
4042 );
4043 } elseif ($languageOriginal) {
4044 $rows = $wpdb->get_results(
4045 $wpdb->prepare(
4046 "SELECT translated_alias AS pagelink, pagelink AS translated_alias
4047 FROM {$table}
4048 WHERE languageoriginal = %s
4049 AND published = 1",
4050 $languageOriginal
4051 ),
4052 ARRAY_A
4053 );
4054 } else {
4055 $response['result'] = false;
4056 echo wp_json_encode($response);
4057 exit;
4058 }
4059
4060 if ($rows) {
4061 $encodedResult = [];
4062
4063 foreach ($rows as $row) {
4064
4065 // Normalizza WordPress-style (CON trailing slash)
4066 $pagelink = gpt_trailingslashit_url($row['pagelink']);
4067 $translatedAlias = !empty($row['translated_alias']) ? gpt_trailingslashit_url($row['translated_alias']) : '';
4068
4069 // Encode pagelink (solo path)
4070 $parsedUrl = wp_parse_url($pagelink);
4071 $encodedPagelink = $pagelink;
4072
4073 if (!empty($parsedUrl['path'])) {
4074 $pathParts = explode('/', $parsedUrl['path']);
4075 $encodedParts = array_map('rawurlencode', $pathParts);
4076 $encodedPath = implode('/', $encodedParts);
4077
4078 $encodedPagelink = ($parsedUrl['scheme'] ?? '') . '://' . ($parsedUrl['host'] ?? '');
4079 $encodedPagelink .= $encodedPath;
4080 if (!empty($parsedUrl['query'])) {
4081 $encodedPagelink .= '?' . $parsedUrl['query'];
4082 }
4083 if (!empty($parsedUrl['fragment'])) {
4084 $encodedPagelink .= '#' . $parsedUrl['fragment'];
4085 }
4086 }
4087
4088 // Encode translated alias
4089 $encodedAlias = $translatedAlias;
4090 if (!empty($translatedAlias)) {
4091 $parsedAlias = wp_parse_url($translatedAlias);
4092 if (!empty($parsedAlias['path'])) {
4093 $pathParts = explode('/', $parsedAlias['path']);
4094 $encodedParts = array_map('rawurlencode', $pathParts);
4095 $encodedPath = implode('/', $encodedParts);
4096
4097 $encodedAlias = ($parsedAlias['scheme'] ?? '') . '://' . ($parsedAlias['host'] ?? '');
4098 $encodedAlias .= $encodedPath;
4099 if (!empty($parsedAlias['query'])) {
4100 $encodedAlias .= '?' . $parsedAlias['query'];
4101 }
4102 if (!empty($parsedAlias['fragment'])) {
4103 $encodedAlias .= '#' . $parsedAlias['fragment'];
4104 }
4105 }
4106 }
4107
4108 $encodedResult[$encodedPagelink] = [
4109 'pagelink' => $encodedPagelink,
4110 'translated_alias' => $encodedAlias
4111 ];
4112 }
4113
4114 $response['result'] = true;
4115 $response['translated_aliases'] = $encodedResult;
4116
4117 } else {
4118 $response['result'] = false;
4119 }
4120
4121 } catch (Exception $e) {
4122 $response['result'] = false;
4123 $response['exception'] = $e->getMessage();
4124 }
4125 } elseif ( $task === 'deepseektranslations' ) {
4126 try {
4127 // Read raw JSON payload sent from JS
4128 $rawInput = file_get_contents( 'php://input' );
4129 $requestData = json_decode( $rawInput, true );
4130
4131 if ( ! $requestData || empty( $requestData['messages'] ) ) {
4132 throw new Exception( 'Invalid DeepSeek request payload' );
4133 }
4134
4135 // Get plugin options
4136 $opts = get_option( 'gptranslate_options', [] );
4137
4138 $deepseekApiKey = $opts['chatgpt_apikey'] ?? '';
4139 $deepseekModel = $opts['chatgpt_model'] ?? 'deepseek-chat';
4140
4141 if ( empty( $deepseekApiKey ) ) {
4142 throw new Exception( 'DeepSeek API key not configured' );
4143 }
4144
4145 // Fixed DeepSeek parameters (server controlled)
4146 $payload = [
4147 'model' => $deepseekModel,
4148 'messages' => $requestData['messages'],
4149 'max_tokens' => 4096,
4150 'temperature' => 0.5,
4151 ];
4152
4153 // Call DeepSeek API
4154 $ch = curl_init( 'https://api.deepseek.com/v1/chat/completions' );
4155 curl_setopt_array( $ch, [
4156 CURLOPT_POST => true,
4157 CURLOPT_RETURNTRANSFER => true,
4158 CURLOPT_HTTPHEADER => [
4159 'Content-Type: application/json',
4160 'Authorization: Bearer ' . $deepseekApiKey,
4161 ],
4162 CURLOPT_POSTFIELDS => wp_json_encode( $payload ),
4163 CURLOPT_TIMEOUT => 60,
4164 ] );
4165
4166 $apiResponse = curl_exec( $ch );
4167
4168 if ( $apiResponse === false ) {
4169 throw new Exception( 'DeepSeek cURL error: ' . curl_error( $ch ) );
4170 }
4171
4172 $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
4173 curl_close( $ch );
4174
4175 if ( $httpCode >= 400 ) {
4176 throw new Exception( 'DeepSeek API HTTP error: ' . $httpCode );
4177 }
4178
4179 // IMPORTANT:
4180 // Return DeepSeek response EXACTLY as the API returns it
4181 header( 'Content-Type: application/json; charset=utf-8' );
4182 echo $apiResponse;
4183 exit;
4184
4185 } catch ( Exception $e ) {
4186 // FALLBACK SAFE RESPONSE (OpenAI-compatible)
4187 header('Content-Type: application/json; charset=utf-8');
4188 echo wp_json_encode([
4189 'choices' => [[
4190 'finish_reason' => 'error',
4191 'message' => [
4192 'role' => 'assistant',
4193 'content' => ''
4194 ]
4195 ]]
4196 ]);
4197 exit;
4198 }
4199 } elseif ($task === 'deepltranslations') {
4200 try {
4201 // Use $params already parsed by WordPress REST API (php://input is consumed)
4202 if (empty ( $params ['texts'] )) {
4203 throw new Exception ( 'Invalid DeepL request payload' );
4204 }
4205
4206 // Get plugin options
4207 $opts = get_option ( 'gptranslate_options', [ ] );
4208
4209 $deeplApiKey = $opts ['chatgpt_apikey'] ?? '';
4210
4211 if (empty ( $deeplApiKey )) {
4212 throw new Exception ( 'DeepL API key not configured' );
4213 }
4214
4215 $sourceLanguage = sanitize_text_field ( $params ['source_lang'] ?? 'auto');
4216 $targetLanguage = sanitize_text_field ( $params ['target_lang'] ?? 'EN');
4217
4218 // Build query string manually - DeepL requires repeated "text" params (not text[0], text[1])
4219 $queryParts = [ ];
4220 $queryParts [] = 'source_lang=' . urlencode ( strtoupper ( $sourceLanguage ) );
4221 $queryParts [] = 'target_lang=' . urlencode ( strtoupper ( $targetLanguage ) );
4222
4223 foreach ( $params ['texts'] as $text ) {
4224 $queryParts [] = 'text=' . urlencode ( $text );
4225 }
4226
4227 $queryString = implode ( '&', $queryParts );
4228
4229 // Call DeepL API - Auto-detect endpoint based on API key type
4230 // Free API keys end with ':fx', paid keys don't have this suffix
4231 $deeplEndpoint = (strpos ( $deeplApiKey, ':fx' ) !== false) ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
4232 $ch = curl_init ( $deeplEndpoint . '/v2/translate' );
4233 curl_setopt_array ( $ch, [
4234 CURLOPT_POST => true,
4235 CURLOPT_RETURNTRANSFER => true,
4236 CURLOPT_HTTPHEADER => [
4237 'Authorization: DeepL-Auth-Key ' . $deeplApiKey,
4238 'Content-Type: application/x-www-form-urlencoded'
4239 ],
4240 CURLOPT_POSTFIELDS => $queryString,
4241 CURLOPT_TIMEOUT => 60
4242 ] );
4243
4244 $apiResponse = curl_exec ( $ch );
4245
4246 if ($apiResponse === false) {
4247 throw new Exception ( 'DeepL cURL error: ' . curl_error ( $ch ) );
4248 }
4249
4250 $httpCode = curl_getinfo ( $ch, CURLINFO_HTTP_CODE );
4251 curl_close ( $ch );
4252
4253 if ($httpCode >= 400) {
4254 throw new Exception ( 'DeepL API HTTP error: ' . $httpCode . ' - ' . $apiResponse );
4255 }
4256
4257 // Return DeepL response EXACTLY as the API returns it
4258 header ( 'Content-Type: application/json; charset=utf-8' );
4259 echo $apiResponse;
4260 exit ();
4261 } catch ( Exception $e ) {
4262 wp_send_json_error ( [
4263 'message' => $e->getMessage ()
4264 ] );
4265 }
4266 }
4267
4268 return rest_ensure_response( $response );
4269 }
4270
4271 // Instantiate and run the app
4272 GPTranslate::get_instance();
4273
4274 // Optimizer compatibility – Phase 1 (early).
4275 // PHP-side exclusion filters must be registered at plugin load time so they
4276 // are in place before any optimizer (e.g. WP Rocket) reads them during its
4277 // own initialisation. Never runs in wp-admin (handled inside init_early).
4278 GPTranslate_Optimizer_Compat::init_early();
4279
4280 // Optimizer compatibility – Phase 2 (late).
4281 // HTML tag-manipulation filters (script_loader_tag, wp_inline_script_tag)
4282 // are registered here, after GPTranslate has decided whether to enqueue
4283 // scripts on this page. Bails immediately if gptranslate-main is not queued.
4284 add_action( 'wp_enqueue_scripts', [ 'GPTranslate_Optimizer_Compat', 'init_late' ], 999 );