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