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