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