PluginProbe ʕ •ᴥ•ʔ
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI / 2.34.2
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI v2.34.2
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 / serverside-translations.php
gptranslate Last commit date
assets 4 days ago flags 4 days ago includes 4 days ago language 4 days ago ajax-handler.php 4 days ago gptranslate.php 1 day ago multilang-routing.php 4 days ago readme.txt 1 day ago serverside-translations.php 4 days ago settings.php 4 days ago simplehtmldom.php 4 days ago uninstall.php 4 days ago
serverside-translations.php
2279 lines
1 <?php
2 if (!defined('ABSPATH')) exit;
3
4 // Hook to apply server-side translations before output is sent to the browser
5 add_action ( 'template_redirect', function () {
6 // Some themes/plugins (e.g. certain LMS course player templates) trigger the
7 // template_redirect action more than once within the same request. This closure
8 // declares several named global functions (normalizeText, processTextNodes, etc.);
9 // running it again would attempt to redeclare them and cause a PHP fatal error.
10 static $gptranslate_serverside_already_ran = false;
11 if ($gptranslate_serverside_already_ran) {
12 return;
13 }
14 $gptranslate_serverside_already_ran = true;
15
16 if (is_admin ()) {
17 return;
18 }
19
20 $raw_request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
21 $uri = esc_url_raw( $raw_request_uri );
22
23 $settings = get_option ( 'gptranslate_options', [ ] );
24
25 if (empty ( $settings ['serverside_translations'] ) || $settings ['serverside_translations'] != 1)
26 return;
27
28 // Skip static assets and admin/API routes
29 if (preg_match ( '#\.(ico|png|jpe?g|gif|svg|css|js|woff2?|ttf|eot|mp4|webm)$#i', $uri ) || strpos ( $uri, '/wp-' ) === 0 || strpos ( $uri, '/wp-json/' ) === 0) {
30 return;
31 }
32
33 function normalizeText($text) {
34 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
35 $text = str_replace(["\xC2\xA0", "\n", "\r", "\t"], ' ', $text); // NBSP e spazi strani
36 $text = preg_replace('/\s+/u', ' ', $text); // spazi multipli → singolo spazio
37 $text = trim($text);
38 return $text;
39 }
40
41 // Normalizza senza trim per preservare spazi iniziali/finali (utile per SimpleHTMLDOM).
42 function normalizeTextNoTrim($text) {
43 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
44 $text = str_replace(["\xC2\xA0", "\n", "\r", "\t"], ' ', $text); // NBSP e spazi strani
45 $text = preg_replace('/\s+/u', ' ', $text); // spazi multipli -> singolo spazio
46 return $text;
47 }
48
49 // Normalizza il testo per l'incremental translation detection con dictionary replacement
50 // Applica le stesse trasformazioni del client-side JS per far corrispondere i testi nel DB
51 function normalizeTextForIncremental($text, $settings, $originalLang, $translatedLang) {
52 // Normalizzazione base
53 $text = normalizeText($text);
54
55 // Il dictionary viene decodificato e filtrato per coppia di lingue una sola volta:
56 // questa funzione gira per ogni nodo di testo della pagina.
57 static $compiledRulesCache = [];
58 $cacheKey = $originalLang . '|' . $translatedLang;
59
60 if (!isset($compiledRulesCache[$cacheKey])) {
61 $compiledRules = [];
62
63 // Carica il dictionary come array (è una stringa JSON)
64 $dictionary = isset($settings['words_leafnodes_excluded_bylanguage_repeatable'])
65 ? $settings['words_leafnodes_excluded_bylanguage_repeatable']
66 : [];
67
68 // Decodifica la stringa JSON se necessario
69 if (is_string($dictionary)) {
70 $dictionary = json_decode($dictionary, true);
71 }
72
73 if (is_array($dictionary)) {
74 // Iterare su tutte le entries del dictionary
75 foreach ($dictionary as $entry) {
76 // Filtrare per lingua (fallback a "*" per all languages)
77 $langOriginal = isset($entry['langOriginal']) ? $entry['langOriginal'] : '*';
78 $langTranslated = isset($entry['langTranslated']) ? $entry['langTranslated'] : '*';
79
80 // Verifica che le lingue corrispondano (considerando "*" come wildcard)
81 if (($langOriginal !== '*' && $langOriginal !== $originalLang) || ($langTranslated !== '*' && $langTranslated !== $translatedLang)) {
82 continue;
83 }
84
85 // Parse words (CSV format: "word1,word2,word3")
86 $words = isset($entry['word']) ? array_map('trim', explode(',', $entry['word'])) : [];
87
88 // Parse translations (CSV format: "trans1,trans2,trans3")
89 $translations = isset($entry['optionalTranslation']) ? array_map('trim', explode(',', $entry['optionalTranslation'])) : [];
90
91 // Applica sostituzioni per indice (word[0] -> translation[0], ecc.)
92 foreach ($words as $i => $word) {
93 if (empty($word)) continue;
94
95 $replacement = isset($translations[$i]) ? $translations[$i] : '';
96 if (empty($replacement)) continue;
97
98 // Word-boundary matching: sostituisce solo le parole intere, non substring
99 // Esempio: 'di' non sostituisce 'di' dentro 'modify' o 'indifferenziato'
100 $compiledRules[] = [
101 'pattern' => '/\b' . preg_quote($word, '/') . '\b/ui',
102 'replacement' => $replacement,
103 ];
104 }
105 }
106 }
107
108 $compiledRulesCache[$cacheKey] = $compiledRules;
109 }
110
111 foreach ($compiledRulesCache[$cacheKey] as $rule) {
112 $text = preg_replace($rule['pattern'], $rule['replacement'], $text);
113 }
114
115 return $text;
116 }
117
118 // Incremental exact-match applicato a un singolo nodo di testo, condiviso dai metodi
119 // regex / domdocument / simplehtmldom.
120 // Il testo viene sostituito solo su corrispondenza intera (o per frammenti delimitati
121 // dalle excluded words), mai a substring: contenuto nuovo resta intatto invece di
122 // essere corrotto da chiavi corte accumulate nel DB dalle incremental translations.
123 // $incr e' il contesto precalcolato una volta per pagina (vedi gpt_build_incremental_context).
124 function gpt_incremental_translate_text($text, $incr) {
125 // Extract leading/trailing whitespace from original text
126 preg_match('/^(\s*)(.*?)(\s*)$/s', $text, $wsMatches);
127 $leadingWhitespace = $wsMatches[1] ?? '';
128 $trailingWhitespace = $wsMatches[3] ?? '';
129
130 // Apply dictionary replacement for incremental matching (matches client-side behavior)
131 $normalizedText = normalizeTextForIncremental($text, $incr['settings'], $incr['originalLang'], $incr['translatedLang']);
132 if ($incr['caseInsensitive']) {
133 $normalizedText = mb_strtolower($normalizedText, 'UTF-8');
134 }
135 $normalizedText = trim($normalizedText);
136
137 // Step 1: Exact match
138 if (isset($incr['lookup'][$normalizedText])) {
139 return $leadingWhitespace . $incr['lookup'][$normalizedText] . $trailingWhitespace;
140 }
141
142 // Step 2: Excluded-words-aware fragment matching
143 // Mirrors client-side wrap_excluded_words behavior: client creates SEPARATE text nodes
144 // around excluded words, so DB stores fragments separately, NOT the full text.
145 // Example: "Now i add a new text and modify the page" with 'text' excluded
146 // Client creates 3 nodes: "Now i add a new ", "text" (preserved), " and modify the page"
147 // DB stores: "Now i add a new " -> "Ora aggiungo un nuovo ", " and modify the page" -> " e modificare la pagina"
148 // We split the text by excluded words, translate each fragment, preserve excluded words.
149 if ($incr['excludedPattern'] === '') {
150 return $text;
151 }
152
153 // Split keeping excluded words as delimiters (PREG_SPLIT_DELIM_CAPTURE).
154 // Always case-insensitive here, regardless of $incr['caseInsensitive']: the client
155 // protects/wraps excluded words with a hardcoded case-insensitive match (ssProtectExcludedWords
156 // and the original wrap_excluded_words splitting both use the 'i' flag unconditionally), so a
157 // mixed-case source word (e.g. "Magnets" ... "magnets") is already split into separate DB
158 // fragments on the client side. Splitting here case-sensitively would leave the lowercase
159 // occurrences bundled into a larger segment that matches no stored key, and the paragraph
160 // would fall through untranslated even though every piece is already known.
161 $parts = preg_split(
162 '/((?<!\w)(?:' . $incr['excludedPattern'] . ')(?!\w))/ui',
163 $text,
164 -1,
165 PREG_SPLIT_DELIM_CAPTURE
166 );
167
168 if (!is_array($parts) || count($parts) < 2) {
169 return $text;
170 }
171
172 $rebuiltText = '';
173 $anyTranslated = false;
174
175 foreach ($parts as $part) {
176 if ($part === '') {
177 continue;
178 }
179
180 // Check if this part is an excluded word - preserve as-is
181 $excludedSetKey = strtolower(trim($part));
182 if (isset($incr['excludedWordsSet'][$excludedSetKey])) {
183 $replacement = $incr['replacementMap'][$incr['excludedWordsSet'][$excludedSetKey]] ?? '';
184 if ($replacement !== '') {
185 $rebuiltText .= $replacement;
186 $anyTranslated = true;
187 } else {
188 $rebuiltText .= $part;
189 }
190 continue;
191 }
192
193 // Try to find an exact match for this fragment in DB
194 // Capture the fragment's own leading/trailing whitespace before trimming for the
195 // lookup key, same as the whole-text match above: the stored value never carries
196 // this whitespace (it was trimmed when saved), so pasting it in without restoring
197 // the fragment's original spacing glues it straight onto the adjacent word (e.g.
198 // "Magnetsysteme" + " und Baugruppen" -> "sistemi magnetici" + "e gruppi" becomes
199 // "sistemi magneticie gruppi" with no space at the join).
200 preg_match('/^(\s*)(.*?)(\s*)$/s', $part, $partWsMatches);
201 $partLeadingWhitespace = $partWsMatches[1] ?? '';
202 $partTrailingWhitespace = $partWsMatches[3] ?? '';
203
204 $normalizedPart = normalizeTextForIncremental($part, $incr['settings'], $incr['originalLang'], $incr['translatedLang']);
205 if ($incr['caseInsensitive']) {
206 $normalizedPart = mb_strtolower($normalizedPart, 'UTF-8');
207 }
208 $normalizedPart = trim($normalizedPart);
209
210 if ($normalizedPart !== '' && isset($incr['lookup'][$normalizedPart])) {
211 $rebuiltText .= $partLeadingWhitespace . $incr['lookup'][$normalizedPart] . $partTrailingWhitespace;
212 $anyTranslated = true;
213 } else {
214 // Fragment not found in DB - keep original
215 $rebuiltText .= $part;
216 }
217 }
218
219 return $anyTranslated ? $rebuiltText : $text;
220 }
221
222 ob_start ( function ($html) use ($uri, $settings) {
223 global $wpdb;
224
225 $originalLang = $settings ['language'] ?? '';
226
227 if (! defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
228 return $html;
229 }
230
231 $translatedLang = GPTRANSLATE_CURRENT_LANG;
232
233 if (empty ( $originalLang ) || $originalLang === $translatedLang)
234 return $html;
235
236 if ($settings['subfolder_installation'] && ! $settings ['rewrite_language_alias']) {
237 $uri_parts = explode('/', ltrim($uri, '/'));
238
239 // Remove the first part (subfolder name)
240 $subfolder_prefix = '/' . array_shift($uri_parts);
241
242 // Rebuild URI without subfolder
243 $uri = '/' . implode('/', $uri_parts);
244 }
245
246 $pageLink = rtrim ( get_site_url (), '/' ) . '/' . ltrim ( GPTRANSLATE_CURRENT_LANG, '/' ) . $uri;
247
248 if ( $settings ['serverside_translations_urldecode'] ) {
249 $pageLink = urldecode ( $pageLink );
250 }
251
252 if ($settings ['serverside_translations_ignore_querystring'] == 1) {
253 // Remove query string
254 $pageLink = strtok ( $pageLink, '?' );
255 } elseif (! empty ( $_SERVER ['QUERY_STRING'] )) {
256 // Sanitize and rebuild query string
257 $raw_qs = isset($_SERVER['QUERY_STRING']) ? wp_unslash($_SERVER['QUERY_STRING']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
258
259 parse_str ( $raw_qs, $qs_args );
260
261 $clean_args = [ ];
262 foreach ( $qs_args as $key => $value ) {
263 $clean_key = sanitize_key ( $key );
264 $clean_value = is_array ( $value ) ? array_map ( 'sanitize_text_field', $value ) : sanitize_text_field ( $value );
265 $clean_args [$clean_key] = $clean_value;
266 }
267
268 $clean_qs = http_build_query ( $clean_args, '', '&', PHP_QUERY_RFC3986 );
269
270 if ($clean_qs !== '') {
271 $pageLink = strtok ( $pageLink, '?' ) . '?' . $clean_qs;
272 }
273 }
274
275 // Strip entire query string if any server-injected parameter is detected (e.g. q, fbclid)
276 // Acts as a conditional ignore_querystring=1 triggered by specific parameter names
277 if (!empty($settings['serverside_translations_strip_querystring_params']) && strpos($pageLink, '?') !== false) {
278 $stripParams = array_map('trim', explode(',', strtolower($settings['serverside_translations_strip_querystring_params'])));
279 $pageLinkQs = parse_url($pageLink, PHP_URL_QUERY);
280 if ($pageLinkQs) {
281 parse_str($pageLinkQs, $qsArgs);
282 foreach ($stripParams as $param) {
283 if (isset($qsArgs[$param])) {
284 $pageLink = strtok($pageLink, '?');
285 break;
286 }
287 }
288 }
289 }
290
291 if ( $settings ['serverside_translations_urlencode_space'] ) {
292 $pageLink = str_ireplace ( ' ', '%20', $pageLink );
293 }
294
295 $table = $wpdb->prefix . 'gptranslate';
296
297 if( $settings ['rewrite_language_url'] && $settings ['rewrite_language_alias'] ) {
298 $row = $wpdb->get_row ( $wpdb->prepare ( "SELECT pagelink, translations, alt_translations FROM {$wpdb->prefix}gptranslate" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
299 "\n WHERE ( pagelink = %s OR pagelink = %s OR translated_alias = %s OR translated_alias = %s) AND languageoriginal = %s AND languagetranslated = %s AND published = 1",
300 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $originalLang, $translatedLang ), ARRAY_A );
301 } else {
302 $row = $wpdb->get_row ( $wpdb->prepare ( "SELECT pagelink, translations, alt_translations FROM {$wpdb->prefix}gptranslate" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
303 "\n WHERE ( pagelink = %s OR pagelink = %s ) AND languageoriginal = %s AND languagetranslated = %s AND published = 1",
304 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $originalLang, $translatedLang ), ARRAY_A );
305 }
306
307 if (! $row) {
308 return $html;
309 }
310
311 // Load translated aliases to replace on page links
312 $translatedAliasesMap = []; // Map absolute URLs
313 $translatedAliasesRelativeMap = []; // Map relative URL (pathname)
314
315 // Store settings for keep context flatten tags
316 $flattenInnerFormattingTags = isset($settings['flatten_inner_formatting_tags']) ? $settings['flatten_inner_formatting_tags'] : 0;
317 $flattenInnerFormattingTagsToRemove = isset($settings['flatten_inner_formatting_tags_to_remove']) ? explode(',', $settings['flatten_inner_formatting_tags_to_remove']) : explode(',', 'strong,em,u,b,i');
318 $wrapExcludedWords = isset($settings['wrap_excluded_words']) ? $settings['wrap_excluded_words'] : 0;
319
320 if (!empty($settings['rewrite_page_links']) && !empty($settings['rewrite_language_url']) && !empty($settings['rewrite_language_alias'])) {
321 try {
322 $aliasesResult = $wpdb->get_results( $wpdb->prepare(
323 "SELECT pagelink, translated_alias" .
324 "\n FROM {$table}" .
325 "\n WHERE languagetranslated = %s" .
326 "\n AND published = 1",
327 $translatedLang
328 ), ARRAY_A );
329
330 if ($aliasesResult) {
331 $parsedRoot = trailingslashit(get_site_url());
332
333 foreach ($aliasesResult as $rowAliasResult) {
334 // NORMALIZE, WORDPRESS STYLE (KEEP TRAILING SLASH)
335 $pagelink = gpt_trailingslashit_url($rowAliasResult['pagelink']);
336 $translatedAlias = $rowAliasResult['translated_alias'] ?? '';
337
338 if (empty($translatedAlias)) continue;
339
340 $translatedAlias = gpt_trailingslashit_url($translatedAlias);
341
342 // First pass: use decoded pagelink and alias as key (DB stores decoded UTF-8)
343 $translatedAliasesMap[$pagelink] = $translatedAlias;
344
345 // Second pass: use decoded relative path as key
346 $relativePath = str_replace($parsedRoot, '/', $pagelink);
347 $relativePath = parse_url($relativePath, PHP_URL_PATH);
348 if ($relativePath) {
349 $translatedAliasesRelativeMap[gpt_trailingslashit_url($relativePath)] = $translatedAlias;
350 }
351 }
352 }
353 } catch (Exception $e) {
354 // Silently fail
355 }
356 }
357
358 // Check if incremental mode is enabled and preserve new texts option is active
359 $incrementalEnabledPreserveNewTexts = !empty($settings['incremental_enabled']) && !empty($settings['incremental_preserve_new_texts']);
360
361 // Load excluded words list for incremental preserve mode
362 // When incremental_preserve_new_texts is ON, we use excluded words to do a "partial greedy match":
363 // - strip excluded words from the segment, then compare remainder with DB keys
364 // - this lets "Test new post" (Test=excluded) match DB key "new post" → "nuovo post"
365 // - without accidentally applying partial translations like "now"→"ora" to new untranslated text
366 $excludedWordsList = [];
367 $excludedWordsReplacementMap = [];
368 if ($incrementalEnabledPreserveNewTexts) {
369 // 1. Simple excluded words (words_leafnodes_excluded)
370 $rawExcluded = $settings['words_leafnodes_excluded'] ?? '';
371 $rawExcluded = str_ireplace(["\r", "\n"], ",", $rawExcluded);
372 $rawExcluded = preg_replace('/,+/', ',', $rawExcluded);
373 $excludedWordsList = array_filter(array_map('trim', explode(',', $rawExcluded)));
374 foreach ($excludedWordsList as $word) {
375 $key = mb_strtolower(trim($word), 'UTF-8');
376 if ($key !== '' && !isset($excludedWordsReplacementMap[$key])) {
377 $excludedWordsReplacementMap[$key] = '';
378 }
379 }
380
381 // 2. Dictionary (words_leafnodes_excluded_bylanguage_repeatable)
382 // Each entry has: word (CSV), langOriginal, langTranslated, optionalTranslation
383 // Only include entries that match the current language pair (* = any language)
384 $dictionaryRaw = $settings['words_leafnodes_excluded_bylanguage_repeatable'] ?? '[]';
385 if (is_string($dictionaryRaw)) {
386 $dictionary = json_decode($dictionaryRaw, true) ?? [];
387 } else {
388 $dictionary = (array) $dictionaryRaw;
389 }
390 foreach ($dictionary as $entry) {
391 $entryLangOrig = $entry['langOriginal'] ?? '*';
392 $entryLangTarget = $entry['langTranslated'] ?? '*';
393 $matchesOrig = ($entryLangOrig === '*' || $entryLangOrig === $originalLang);
394 $matchesTarget = ($entryLangTarget === '*' || $entryLangTarget === $translatedLang);
395 if ($matchesOrig && $matchesTarget) {
396 $words = array_filter(array_map('trim', explode(',', $entry['word'] ?? '')));
397 $translations = array_map('trim', explode(',', $entry['optionalTranslation'] ?? ''));
398 foreach ($words as $i => $word) {
399 if (!empty($word)) {
400 $excludedWordsList[] = $word;
401 $key = mb_strtolower(trim($word), 'UTF-8');
402 $replacement = isset($translations[$i]) ? $translations[$i] : '';
403 if ($key !== '' && !isset($excludedWordsReplacementMap[$key])) {
404 $excludedWordsReplacementMap[$key] = $replacement;
405 } elseif ($key !== '' && $excludedWordsReplacementMap[$key] === '' && $replacement !== '') {
406 $excludedWordsReplacementMap[$key] = $replacement;
407 }
408 }
409 }
410 }
411 }
412
413 $excludedWordsList = array_unique($excludedWordsList);
414 }
415
416 // Precalcola una sola volta per pagina tutto cio' che serve all'incremental exact-match.
417 // Prima la normalizzazione col dictionary veniva rifatta per ogni chiave del DB su ogni
418 // nodo di testo, rendendo il costo O(nodi x traduzioni x dictionary): su pagine lunghe
419 // con molte traduzioni accumulate questo poteva mandare la richiesta in timeout.
420 $incrementalContext = null;
421 if ($incrementalEnabledPreserveNewTexts) {
422 $incrCaseInsensitive = ! empty ( $settings ['serverside_translations_caseinsensitive'] );
423
424 $incrTranslations = json_decode ( $row ['translations'], true ) ?? [ ];
425 // NO sort here: when two DB keys normalize to the same value, the original
426 // sequential loop picked whichever came FIRST in json_decode's (insertion) order,
427 // and "first wins" below only reproduces that if the array keeps its natural order.
428 $incrementalLookup = [];
429 foreach ( $incrTranslations as $originalText => $translatedText ) {
430 $normalizedOrig = normalizeTextForIncremental($originalText, $settings, $originalLang, $translatedLang);
431 if ($incrCaseInsensitive) {
432 $normalizedOrig = mb_strtolower($normalizedOrig, 'UTF-8');
433 }
434 $normalizedOrig = trim($normalizedOrig);
435 if (!isset($incrementalLookup[$normalizedOrig])) {
436 $incrementalLookup[$normalizedOrig] = $translatedText;
437 }
438 }
439
440 // La membership test originale usava strcasecmp (ASCII), mentre la chiave della
441 // replacement map e' mb_strtolower: la mappa qui conserva entrambe le forme.
442 // Ordina per lunghezza decrescente prima di costruire l'alternanza regex: altrimenti
443 // una voce piu' corta (es. "Haftkraft") puo' precedere nell'alternanza una voce piu'
444 // lunga e piu' specifica che la contiene come prefisso (es. "Haftkraft berechnen"),
445 // e la regex si ferma al primo match valido invece che al piu' lungo/corretto.
446 $excludedWordsListSorted = $excludedWordsList;
447 usort($excludedWordsListSorted, fn($a, $b) => mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8'));
448
449 $incrementalExcludedWordsSet = [];
450 $incrementalExcludedPattern = '';
451 foreach ($excludedWordsListSorted as $excludedWord) {
452 $excludedWord = trim($excludedWord);
453 if ($excludedWord === '') {
454 continue;
455 }
456 $incrementalExcludedPattern .= ($incrementalExcludedPattern ? '|' : '') . preg_quote($excludedWord, '/');
457 $setKey = strtolower($excludedWord);
458 if (!isset($incrementalExcludedWordsSet[$setKey])) {
459 $incrementalExcludedWordsSet[$setKey] = mb_strtolower($excludedWord, 'UTF-8');
460 }
461 }
462
463 $incrementalContext = [
464 'settings' => $settings,
465 'originalLang' => $originalLang,
466 'translatedLang' => $translatedLang,
467 'caseInsensitive' => $incrCaseInsensitive,
468 'lookup' => $incrementalLookup,
469 'excludedPattern' => $incrementalExcludedPattern,
470 'excludedWordsSet' => $incrementalExcludedWordsSet,
471 'replacementMap' => $excludedWordsReplacementMap,
472 ];
473 }
474
475 // Process replacement method
476 if ($settings ['serverside_translations_method'] == 'regex') {
477 $translations = json_decode ( $row ['translations'], true ) ?? [ ];
478 $altTranslations = json_decode ( $row ['alt_translations'], true ) ?? [ ];
479 uksort ( $translations, fn ($a, $b) => strlen ( $b ) - strlen ( $a ) );
480 uksort ( $altTranslations, fn ($a, $b) => strlen ( $b ) - strlen ( $a ) );
481
482 // Flatten formatting tags in the body to improve matching if enabled
483 if ($flattenInnerFormattingTags) {
484 $regexTagsToRemove = implode('|', array_map('preg_quote', $flattenInnerFormattingTagsToRemove));
485 function gp_flatten_formatting_tags_regex($html, $regexTagsToRemove) {
486 $pattern = '~<(' . $regexTagsToRemove . ')(\s[^>]*)?>(.*?)</\1>~is';
487
488 // Ripeti finché non ci sono più match (gestisce annidamenti semplici)
489 $prev = null;
490 while ($prev !== $html) {
491 $prev = $html;
492 $html = preg_replace_callback($pattern, function ($m) {
493 // Preserva elementi che contengono form elements o media (select, input, textarea, button, img, ecc.)
494 if (preg_match('/<(select|input|textarea|button|option|optgroup|fieldset|datalist|output|label|form|img|video|audio|canvas|svg|iframe)[\s>\/]/i', $m[3])) {
495 return $m[0]; // restituisci il tag originale intatto
496 }
497 // Preserva elementi vuoti (es. <i> FontAwesome/icon fonts)
498 if (trim(str_replace(['&nbsp;', "\xc2\xa0", "\xa0"], '', strip_tags($m[3]))) === '') {
499 return $m[0];
500 }
501 // tieni SOLO il testo interno (se dentro c’è altro HTML lo butta via)
502 $inner = strip_tags($m[3]);
503 return $inner;
504 }, $html);
505 }
506
507 return $html;
508 }
509 $html = gp_flatten_formatting_tags_regex($html, $regexTagsToRemove);
510 }
511
512 $caseInsensitive = ! empty ( $settings ['serverside_translations_caseinsensitive'] );
513 $matchQuotes = ! empty ( $settings ['serverside_translations_matchquotes'] );
514 $excludedPatterns = [ ];
515
516 $excludedCss = preg_replace ( '/,+/', ',', str_ireplace ( [
517 "\r",
518 "\n",
519 '"'
520 ], [
521 ',',
522 ',',
523 ''
524 ], $settings ['css_selector_serverside_leafnodes_excluded'] ?? '') );
525 $excludedCss = array_filter ( array_map ( 'trim', explode ( ',', $excludedCss ) ) );
526
527 foreach ( $excludedCss as $selector ) {
528 if (preg_match ( '/^([a-z0-9]+)\.(.+)$/i', $selector, $m )) {
529 $excludedPatterns [] = '/<' . preg_quote ( $m [1], '/' ) . '(?=[^>]*\sclass\s*=\s*["\'][^"\']*\b' . preg_quote ( $m [2], '/' ) . '\b)[^>]*>/i';
530 } elseif (preg_match ( '/^\.(.+)$/', $selector, $m )) {
531 $excludedPatterns [] = '/<([a-z0-9]+)(?=[^>]*\sclass\s*=\s*["\'][^"\']*\b' . preg_quote ( $m [1], '/' ) . '\b)[^>]*>/i';
532 }
533 }
534
535 // PROTECT script/style content before splitting (if option enabled)
536 $protectedScriptsStyles = [];
537
538 if (!empty($settings['protect_script_style'])) {
539 $protectCounter = 0;
540
541 // Extract <script>...</script> tags
542 $html = preg_replace_callback(
543 '/<script\b[^>]*>.*?<\/script>/is',
544 function ($matches) use (&$protectedScriptsStyles, &$protectCounter) {
545 $placeholder = "___GPTRANSLATE_SCRIPT_" . ($protectCounter++) . "___";
546 $protectedScriptsStyles[$placeholder] = $matches[0];
547 return $placeholder;
548 },
549 $html
550 );
551
552 // Extract <style>...</style> tags
553 $html = preg_replace_callback(
554 '/<style\b[^>]*>.*?<\/style>/is',
555 function ($matches) use (&$protectedScriptsStyles, &$protectCounter) {
556 $placeholder = "___GPTRANSLATE_STYLE_" . ($protectCounter++) . "___";
557 $protectedScriptsStyles[$placeholder] = $matches[0];
558 return $placeholder;
559 },
560 $html
561 );
562 }
563
564 $segments = preg_split ( '/(<[^>]+>)/i', $html, - 1, PREG_SPLIT_DELIM_CAPTURE );
565
566 $skipStack = [ ];
567 foreach ( $segments as $index => $segment ) {
568 if (preg_match ( '/^<\s*(script|style)(\s|>)/i', $segment, $matches )) {
569 $skipStack [] = strtolower ( $matches [1] );
570 } elseif (preg_match ( '/<\/\s*(script|style)[^>]*>/i', $segment, $matches )) { // <--- FIX QUI
571 $tag = strtolower ( $matches [1] );
572 if (! empty ( $skipStack ) && end ( $skipStack ) === $tag) {
573 array_pop ( $skipStack );
574 }
575 } elseif (preg_match ( '/^<\s*([a-zA-Z0-9]+)/', $segment, $tagMatch )) {
576 $tagName = strtolower ( $tagMatch [1] );
577 foreach ( $excludedPatterns as $pattern ) {
578 if (preg_match ( $pattern, $segment )) {
579 $skipStack [] = $tagName;
580 break;
581 }
582 }
583 } elseif (preg_match ( '/^<\/\s*([a-zA-Z0-9]+)/', $segment, $tagMatch )) {
584 $tagName = strtolower ( $tagMatch [1] );
585 if (! empty ( $skipStack ) && end ( $skipStack ) === $tagName) {
586 array_pop ( $skipStack );
587 }
588 } elseif (! preg_match ( '/^<[^>]+>$/', $segment )) {
589 if (empty ( $skipStack )) {
590 if ($matchQuotes) {
591 $segment = str_ireplace ( '"', "'", $segment );
592 }
593 // Normalize once without trimming to preserve spaces between nodes
594 $segment = normalizeTextNoTrim($segment);
595
596 // INCREMENTAL FIX: Use exact match only if incremental preserve new texts is enabled
597 if ($incrementalEnabledPreserveNewTexts) {
598 $segment = gpt_incremental_translate_text($segment, $incrementalContext);
599 } else {
600 // Normal pattern matching (standard behavior)
601 foreach ( $translations as $originalText => $translatedText ) {
602 $originalText = normalizeText($originalText);
603
604 $prevSegment = $segment;
605 if ($wrapExcludedWords) {
606 // Use word boundaries to avoid sub-replacing inside already translated words
607 $pattern = '/' . (preg_match('/^\w/u', trim($originalText)) ? '(?<!\w)' : '') . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
608 $segment = preg_replace($pattern, $translatedText, $segment);
609 } else {
610 // Use word boundaries to avoid sub-replacing inside already translated words
611 $pattern = '/' . (preg_match('/^\w/u', trim($originalText)) ? '(?<!\w)' : '') . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
612 $segment = preg_replace($pattern, $translatedText, $segment);
613 if ($segment !== $prevSegment) {
614 break;
615 }
616 }
617 }
618 }
619 }
620 }
621
622 $segments [$index] = $segment;
623 }
624
625 $html = implode ( '', $segments );
626
627 // RESTORE protected script/style content (if protection was enabled)
628 if (!empty($settings['protect_script_style'])) {
629 foreach ($protectedScriptsStyles as $placeholder => $originalContent) {
630 $html = str_replace($placeholder, $originalContent, $html);
631 }
632 }
633
634 // Replace alt and title in images
635 if (! empty ( $settings ['translate_altimages'] )) {
636 $html = preg_replace_callback ( '/<img[^>]*\b(alt|title)\s*=\s*([\"\'])(.*?)\2[^>]*>/i', function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
637 $attr = $matches [1];
638 $quote = $matches [2];
639 $value = $matches [3];
640 if ($matchQuotes)
641 $value = str_ireplace ( '"', "'", $value );
642 foreach ( $altTranslations as $original => $translated ) {
643 $original = normalizeText($original);
644 $value = normalizeText($value);
645
646 $value = $caseInsensitive ? str_ireplace ( trim ( $original ), $translated, $value ) : str_replace ( trim ( $original ), $translated, $value );
647 }
648 return preg_replace ( '/\b' . $attr . '\s*=\s*["\'].*?["\']/', "$attr=$quote$value$quote", $matches [0] );
649 }, $html );
650
651 // Check if also images 'src' are enabled to be translated
652 if (! empty ( $settings ['translate_srcimages'] )) {
653 $html = preg_replace_callback('/<img\b[^>]*\bsrc\s*=\s*(["\'])(.*?)\1[^>]*>/i', function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
654 $originalTag = $matches[0];
655 $quote = $matches[1];
656 $srcValue = ltrim($matches[2], '/');
657
658 if ($matchQuotes) {
659 $srcValue = str_ireplace('"', "'", $srcValue);
660 }
661
662 $translatedSrcValue = null;
663 foreach ($altTranslations as $originalText => $translatedText) {
664 $originalText = ltrim($originalText, '/');
665 if ($caseInsensitive) {
666 if (strcasecmp(trim($originalText), $srcValue) === 0) {
667 $translatedSrcValue = $translatedText;
668 break;
669 }
670 } else {
671 if (trim($originalText) === $srcValue) {
672 $translatedSrcValue = $translatedText;
673 break;
674 }
675 }
676 }
677
678 // If a translation was found, replace src and srcset
679 if ($translatedSrcValue !== null) {
680 // Replace the src attribute
681 $modifiedTag = preg_replace('/\bsrc\s*=\s*["\'].*?["\']/i', 'src=' . $quote . $translatedSrcValue . $quote, $originalTag);
682
683 // Check if srcset exists and replace it with the translated src value
684 if (preg_match('/\bsrcset\s*=\s*(["\'])(.*?)\1/i', $modifiedTag, $srcsetMatch)) {
685 $srcsetQuote = $srcsetMatch[1];
686 $modifiedTag = preg_replace('/\bsrcset\s*=\s*["\'].*?["\']/i', 'srcset=' . $srcsetQuote . $translatedSrcValue . $srcsetQuote, $modifiedTag);
687 }
688
689 return $modifiedTag;
690 }
691
692 return $originalTag;
693 }, $html);
694 }
695
696 // Check if also iframes 'src' are enabled to be translated
697 if (! empty ( $settings ['translate_srciframes'] )) {
698 $html = preg_replace_callback('/<iframe\b[^>]*\bsrc\s*=\s*(["\'])(.*?)\1[^>]*>/i', function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
699 $originalTag = $matches[0];
700 $quote = $matches[1];
701 $srcValue = ltrim($matches[2], '/');
702
703 if ($matchQuotes) {
704 $srcValue = str_ireplace('"', "'", $srcValue);
705 }
706
707 $translatedSrcValue = null;
708 foreach ($altTranslations as $originalText => $translatedText) {
709 $originalText = ltrim($originalText, '/');
710 if ($caseInsensitive) {
711 if (strcasecmp(trim($originalText), $srcValue) === 0) {
712 $translatedSrcValue = $translatedText;
713 break;
714 }
715 } else {
716 if (trim($originalText) === $srcValue) {
717 $translatedSrcValue = $translatedText;
718 break;
719 }
720 }
721 }
722
723 if ($translatedSrcValue !== null) {
724 return preg_replace('/\bsrc\s*=\s*["\'].*?["\']/i', 'src=' . $quote . $translatedSrcValue . $quote, $originalTag);
725 }
726
727 return $originalTag;
728 }, $html);
729 }
730
731 // Check if also videos 'src' are enabled to be translated
732 if (! empty ( $settings ['translate_srcvideos'] )) {
733 $html = preg_replace_callback('/<(video|source)\b[^>]*\bsrc\s*=\s*(["\'])(.*?)\2[^>]*>/i', function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
734 $originalTag = $matches[0];
735 $quote = $matches[2];
736 $srcValue = ltrim($matches[3], '/');
737
738 if ($matchQuotes) {
739 $srcValue = str_ireplace('"', "'", $srcValue);
740 }
741
742 $translatedSrcValue = null;
743 foreach ($altTranslations as $originalText => $translatedText) {
744 $originalText = ltrim($originalText, '/');
745 if ($caseInsensitive) {
746 if (strcasecmp(trim($originalText), $srcValue) === 0) {
747 $translatedSrcValue = $translatedText;
748 break;
749 }
750 } else {
751 if (trim($originalText) === $srcValue) {
752 $translatedSrcValue = $translatedText;
753 break;
754 }
755 }
756 }
757
758 if ($translatedSrcValue !== null) {
759 return preg_replace('/\bsrc\s*=\s*["\'].*?["\']/i', 'src=' . $quote . $translatedSrcValue . $quote, $originalTag);
760 }
761
762 return $originalTag;
763 }, $html);
764 }
765 }
766
767 // Translate iframe locale parameter (e.g. Stripe payment forms)
768 if (! empty ( $settings ['translate_iframe_locale'] )) {
769 $currentLangCode = strtolower(explode('-', $currentLanguage)[0]);
770 $html = preg_replace('/(<iframe[^>]*\bsrc=["\'"])([^"\']*?locale=)[a-z]{2}([^"\']*["\'])/i', '$1${2}' . $currentLangCode . '$3', $html);
771 }
772
773 // Translate <meta name="description" content="...">
774 $html = preg_replace_callback(
775 '~<meta\s+(?:name|property)=["\'](?:description|og:description|twitter:description|dc\.description)["\']\s+content=["\'](.*?)["\'][^>]*>~i',
776 function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
777 $originalTag = $matches[0];
778 $contentValue = $matches[1];
779
780 // DECODIFICA HTML entities PRIMA del confronto
781 $contentValue = html_entity_decode($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
782
783 if ($matchQuotes) {
784 $contentValue = str_ireplace('"', "'", $contentValue);
785 }
786
787 foreach ($altTranslations as $originalText => $translatedText) {
788 $prev = $contentValue;
789 if ($caseInsensitive) {
790 $contentValue = str_ireplace(trim($originalText), $translatedText, $contentValue);
791 } else {
792 $contentValue = str_replace(trim($originalText), $translatedText, $contentValue);
793 }
794 if ($contentValue !== $prev) break;
795 }
796
797 // RI-ENCODIFICA per l'HTML output (sicurezza)
798 $contentValue = htmlspecialchars($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
799
800 return preg_replace(
801 '/content=["\'].*?["\']/i',
802 'content="' . $contentValue . '"',
803 $originalTag
804 );
805 },
806 $html
807 );
808
809 // Translate <meta property="og:title"> with the same value as <title> tag
810 // Step 1: Extract the translated title content
811 if (preg_match('~<title[^>]*>(.*?)</title>~is', $html, $titleMatch)) {
812 $translatedTitle = trim(strip_tags($titleMatch[1]));
813
814 // Step 2: Replace og:title content directly
815 $html = preg_replace(
816 '~(<meta\s+property=["\']og:title["\']\s+content=["\']).*?(["\'][^>]*>)~i',
817 '$1' . htmlspecialchars($translatedTitle, ENT_QUOTES, 'UTF-8') . '$2',
818 $html
819 );
820
821 // Step 3: Replace twitter:title content with the same translated title
822 $html = preg_replace(
823 '~(<meta\s+name=["\']twitter:title["\']\s+content=["\']).*?(["\'][^>]*>)~i',
824 '$1' . htmlspecialchars($translatedTitle, ENT_QUOTES, 'UTF-8') . '$2',
825 $html
826 );
827
828 // Step 4: Replace Dublin Core (SEOPress) dc.title content with the same translated title
829 $html = preg_replace(
830 '~(<meta\s+name=["\']dc\.title["\']\s+content=["\']).*?(["\'][^>]*>)~i',
831 '$1' . htmlspecialchars($translatedTitle, ENT_QUOTES, 'UTF-8') . '$2',
832 $html
833 );
834 }
835
836 // Add skip marker
837 $html = preg_replace ( '/<body/i', '<body data-gptranslateskip="1" data-gptranslateoriginalalias="' . $row['pagelink'] . '"', $html, 1 );
838
839 // Replace page links in <a href="..."> tags based on translated aliases
840 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
841 $exclusions = $settings['rewrite_page_links_exclusions'] ?? '';
842 $page_exclusions = $settings['page_exclusions'] ?? '';
843 $html = preg_replace_callback(
844 '/<a\s+([^>]*\s)?href\s*=\s*(["\'])(.*?)\2([^>]*)>/i',
845 function($matches) use ($translatedAliasesMap, $translatedAliasesRelativeMap, $settings, $exclusions, $page_exclusions) {
846 $fullTag = $matches[0];
847 $quote = $matches[2];
848 $href = $matches[3];
849
850 // Check if link should be excluded from rewriting
851 if (gptranslate_should_exclude_link($href, $exclusions, $page_exclusions)) {
852 return $fullTag;
853 }
854
855 // Decode href attribute to match database entries
856 $decodedHref = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
857
858 // Extract hash/fragment if present
859 $hashFragment = '';
860 if (strpos($decodedHref, '#') !== false) {
861 $parts = explode('#', $decodedHref, 2);
862 $decodedHref = $parts[0]; // URL without hash
863 $hashFragment = '#' . $parts[1]; // Save the hash
864 }
865
866 $queryString = '';
867 if ($settings ['ignore_querystring'] == 1 && strpos($decodedHref, '?') !== false) {
868 $parts = explode('?', $decodedHref, 2);
869 $decodedHref = $parts[0];
870 $queryString = '?' . $parts[1];
871 }
872 $originalPathBeforeNormalization = $decodedHref;
873
874 $decodedHref = gpt_trailingslashit_url($decodedHref);
875
876 $translatedAlias = null;
877
878 // First pass, check in absolute URLs
879 if (isset($translatedAliasesMap[$decodedHref])) {
880 $translatedAlias = $translatedAliasesMap[$decodedHref];
881 }
882 // Second pass, check in relative URLs (pathname)
883 elseif (isset($translatedAliasesRelativeMap[$decodedHref])) {
884 $translatedAlias = $translatedAliasesRelativeMap[$decodedHref];
885 }
886
887 // If found a translated alias, replace href and add data-originalhref if not present
888 if ($translatedAlias) {
889 $finalHref = $translatedAlias . $queryString . $hashFragment;
890
891 $originalHrefFull = $originalPathBeforeNormalization . $queryString . $hashFragment;
892
893 // Check if data-originalhref already exists in the tag
894 if (strpos($fullTag, 'data-originalhref') === false) {
895 $dataAttr = ' data-originalhref=' . $quote . htmlspecialchars($originalHrefFull, ENT_QUOTES, 'UTF-8') . $quote;
896 $newTag = str_ireplace('<a ', '<a' . $dataAttr . ' ', $fullTag);
897 $newTag = str_ireplace('href=' . $quote . $href . $quote, 'href=' . $quote . htmlspecialchars($finalHref, ENT_QUOTES, 'UTF-8') . $quote, $newTag);
898 return $newTag;
899 } else {
900 return str_ireplace('href=' . $quote . $href . $quote, 'href=' . $quote . htmlspecialchars($finalHref, ENT_QUOTES, 'UTF-8') . $quote, $fullTag);
901 }
902 }
903
904 return $fullTag;
905 },
906 $html
907 );
908
909 // Replace form action URLs: add language prefix and apply translated aliases
910 if (!empty($settings['rewrite_form_actions'])) {
911 $siteUrl = trailingslashit(get_site_url());
912 $siteHost = parse_url($siteUrl, PHP_URL_HOST);
913 $knownLangs = (isset($settings['languages']) && is_array($settings['languages'])) ? array_map('strtolower', $settings['languages']) : [$originalLang, $translatedLang];
914 $html = preg_replace_callback(
915 '/<form\s+([^>]*\s)?action\s*=\s*(["\'])(.*?)\2([^>]*)>/i',
916 function($matches) use ($translatedAliasesMap, $translatedAliasesRelativeMap, $settings, $translatedLang, $siteUrl, $siteHost, $knownLangs) {
917 $fullTag = $matches[0];
918 $quote = $matches[2];
919 $action = $matches[3];
920
921 $decodedAction = rawurldecode(html_entity_decode($action, ENT_QUOTES, 'UTF-8'));
922
923 // Skip anchors, mailto, tel, javascript, .php endpoints
924 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $decodedAction)) return $fullTag;
925 if (preg_match('/\.php($|\?|\#)/i', $decodedAction)) return $fullTag;
926
927 // Skip external URLs
928 if (preg_match('/^https?:\/\//i', $decodedAction)) {
929 $actionHost = parse_url($decodedAction, PHP_URL_HOST);
930 if ($actionHost !== $siteHost) return $fullTag;
931 }
932
933 // Extract query string
934 $queryString = '';
935 if ($settings['ignore_querystring'] == 1 && strpos($decodedAction, '?') !== false) {
936 $parts = explode('?', $decodedAction, 2);
937 $decodedAction = $parts[0];
938 $queryString = '?' . $parts[1];
939 }
940 $originalActionFull = $decodedAction . $queryString;
941
942 // Extract relative path
943 $actionPath = $decodedAction;
944 $isAbsolute = false;
945 if (preg_match('/^https?:\/\//i', $actionPath)) {
946 $isAbsolute = true;
947 $actionPath = parse_url($actionPath, PHP_URL_PATH) ?: '/';
948 }
949
950 // Split path and determine language index
951 $pathParts = explode('/', $actionPath);
952 $langIndex = 1;
953 if (isset($pathParts[$langIndex]) && $pathParts[$langIndex] === 'index.php') {
954 $langIndex = 2;
955 }
956 if (!empty($settings['subfolder_installation'])) {
957 $langIndex = 2;
958 }
959
960 // Check if already has a language prefix, replace or insert
961 if (isset($pathParts[$langIndex]) && in_array(strtolower($pathParts[$langIndex]), $knownLangs)) {
962 $pathParts[$langIndex] = $translatedLang;
963 } else {
964 array_splice($pathParts, $langIndex, 0, [$translatedLang]);
965 }
966
967 // Rebuild path
968 $rebuiltPath = '/' . implode('/', array_filter($pathParts, function($p) { return $p !== ''; }));
969 $rebuiltPath = gpt_trailingslashit_url($rebuiltPath);
970
971 // Rebuild full URL if original was absolute
972 $computedAction = $isAbsolute ? (rtrim($siteUrl, '/') . $rebuiltPath) : $rebuiltPath;
973 $computedAction = gpt_trailingslashit_url($computedAction);
974
975 // Try alias match
976 $translatedAlias = null;
977 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
978 if (isset($translatedAliasesMap[$computedAction])) {
979 $translatedAlias = $translatedAliasesMap[$computedAction];
980 } elseif (isset($translatedAliasesRelativeMap[$rebuiltPath])) {
981 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPath];
982 } elseif (!$isAbsolute && !empty($settings['subfolder_installation'])) {
983 // Fallback: form action is relative and includes subfolder path
984 // Map keys strip the subfolder, so retry after stripping it
985 $siteBasePath = rtrim(parse_url(rtrim($siteUrl, '/'), PHP_URL_PATH) ?: '', '/');
986 if ($siteBasePath !== '' && strpos($rebuiltPath, $siteBasePath . '/') === 0) {
987 $rebuiltPathNoSubfolder = substr($rebuiltPath, strlen($siteBasePath));
988 if (isset($translatedAliasesRelativeMap[$rebuiltPathNoSubfolder])) {
989 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPathNoSubfolder];
990 }
991 }
992 }
993 }
994
995 $finalAction = $translatedAlias ? ($translatedAlias . $queryString) : ($computedAction . $queryString);
996
997 if (strpos($fullTag, 'data-originalaction') === false) {
998 $dataAttr = ' data-originalaction=' . $quote . htmlspecialchars($originalActionFull, ENT_QUOTES, 'UTF-8') . $quote;
999 $newTag = str_ireplace('<form ', '<form' . $dataAttr . ' ', $fullTag);
1000 $newTag = str_ireplace('action=' . $quote . $action . $quote, 'action=' . $quote . htmlspecialchars($finalAction, ENT_QUOTES, 'UTF-8') . $quote, $newTag);
1001 return $newTag;
1002 } else {
1003 return str_ireplace('action=' . $quote . $action . $quote, 'action=' . $quote . htmlspecialchars($finalAction, ENT_QUOTES, 'UTF-8') . $quote, $fullTag);
1004 }
1005 },
1006 $html
1007 );
1008 } // end rewrite_form_actions
1009 }
1010 } elseif ($settings ['serverside_translations_method'] == 'domdocument') {
1011 // Solution 2: classic DOMDocument approach, effective but could cause closing tags and encoding issues
1012 try {
1013 $translationsArray = json_decode ( $row['translations'], true ) ?? [ ];
1014 $altTranslationsArray = json_decode ( $row['alt_translations'] ?? '', true ) ?: [ ];
1015
1016 // Sort the translation keys in descending order by length.
1017 uksort ( $translationsArray, function ( $a, $b ) {
1018 return strlen ( $b ) - strlen ( $a );
1019 } );
1020
1021 // Sort the alt translation keys in descending order by length.
1022 uksort ( $altTranslationsArray, function ( $a, $b ) {
1023 return strlen ( $b ) - strlen ( $a );
1024 } );
1025
1026 // Flatten formatting tags in the body to improve matching if enabled
1027 if($flattenInnerFormattingTags) {
1028 function gp_flatten_inner_formatting_tags($html, $tagsToRemove) {
1029 $dom = new DOMDocument();
1030 libxml_use_internal_errors(true);
1031
1032 // Usa lo stesso metodo del codice principale con l'hack XML encoding
1033 $dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1034
1035 libxml_clear_errors();
1036
1037 $xpath = new DOMXPath($dom);
1038
1039 // Seleziona tutti i tag da rimuovere, ovunque
1040 $query = '//' . implode(' | //', array_map(function($t) { return strtolower($t); }, $tagsToRemove));
1041
1042 $nodes = $xpath->query($query);
1043
1044 // Importante: sostituire dal "basso verso l'alto" per evitare problemi mentre modifichi il DOM
1045 // Convertiamo NodeList in array e invertiamo
1046 $toReplace = [];
1047 foreach ($nodes as $n)
1048 $toReplace[] = $n;
1049 $toReplace = array_reverse($toReplace);
1050
1051 foreach ($toReplace as $node) {
1052 // Preserva elementi vuoti (es. <i> FontAwesome/icon fonts)
1053 if (trim(str_replace(['&nbsp;', "\xc2\xa0", "\xa0"], '', $node->textContent)) === '') {
1054 continue;
1055 }
1056 // Preserva elementi che contengono form elements (select, input, textarea, button, ecc.)
1057 $interactiveTags = ['select', 'input', 'textarea', 'button', 'option', 'optgroup', 'fieldset', 'datalist', 'output', 'label', 'form', 'img', 'video', 'audio', 'canvas', 'svg', 'iframe'];
1058 $hasInteractive = false;
1059 foreach ($interactiveTags as $iTag) {
1060 if ($node->getElementsByTagName($iTag)->length > 0) {
1061 $hasInteractive = true;
1062 break;
1063 }
1064 }
1065 if ($hasInteractive) {
1066 continue;
1067 }
1068
1069 $text = $node->textContent; // prende il testo "visibile" (quello che vuoi)
1070 $textNode = $dom->createTextNode($text);
1071
1072 if ($node->parentNode) {
1073 $node->parentNode->replaceChild($textNode, $node);
1074 }
1075 }
1076
1077 // Salva l'HTML modificato
1078 $out = $dom->saveHTML();
1079
1080 return $out;
1081 }
1082 $html = gp_flatten_inner_formatting_tags($html, $flattenInnerFormattingTagsToRemove);
1083 }
1084
1085 // Initialize DOMDocument without converting the body content via mb_convert_encoding.
1086 $doc = new DOMDocument ();
1087 libxml_use_internal_errors ( true );
1088 // Use an XML encoding hack to inform DOMDocument about UTF-8 while preserving original characters.
1089
1090 // PROTECT script/style content before DOMDocument loading (if option enabled)
1091 $protectedScriptsStyles = [];
1092 if (!empty($settings['protect_script_style'])) {
1093 $protectCounter = 0;
1094
1095 // Extract <script>...</script> tags and replace with marker meta tags
1096 $html = preg_replace_callback(
1097 '/<script\b[^>]*>.*?<\/script>/is',
1098 function ($matches) use (&$protectedScriptsStyles, &$protectCounter) {
1099 $placeholder = "___GPTRANSLATE_SCRIPT_" . ($protectCounter++) . "___";
1100 $protectedScriptsStyles[$placeholder] = $matches[0];
1101 // Use a data attribute to preserve position - DOMDocument won't move these
1102 return '<meta data-gptranslate-script="' . htmlspecialchars($placeholder, ENT_QUOTES) . '">';
1103 },
1104 $html
1105 );
1106
1107 // Extract <style>...</style> tags and replace with marker meta tags
1108 $html = preg_replace_callback(
1109 '/<style\b[^>]*>.*?<\/style>/is',
1110 function ($matches) use (&$protectedScriptsStyles, &$protectCounter) {
1111 $placeholder = "___GPTRANSLATE_STYLE_" . ($protectCounter++) . "___";
1112 $protectedScriptsStyles[$placeholder] = $matches[0];
1113 // Use a data attribute to preserve position
1114 return '<meta data-gptranslate-style="' . htmlspecialchars($placeholder, ENT_QUOTES) . '">';
1115 },
1116 $html
1117 );
1118 }
1119 $doc->loadHTML ( '<?xml encoding="UTF-8">' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
1120 libxml_clear_errors ();
1121
1122 // Use XPath to locate text nodes, skipping those in <script> or <style> tags.
1123 $xpath = new DOMXPath ( $doc );
1124 $cssSelectorLeafnodesExcluded = str_ireplace ( '"', '', trim ( trim ( preg_replace ( '/,+/', ',', str_ireplace ( [ "\r", "\n" ], ",", $settings['css_selector_serverside_leafnodes_excluded'] ?? '' ) ) ), ',' ) );
1125 $excludedNodes = [ ];
1126
1127 if (! empty ( $cssSelectorLeafnodesExcluded )) {
1128 $selectors = explode ( ',', $cssSelectorLeafnodesExcluded );
1129 $xpathQueries = [ ];
1130
1131 foreach ( $selectors as $selector ) {
1132 $selector = trim ( $selector );
1133 if (empty ( $selector ))
1134 continue;
1135
1136 // Convert CSS selector to XPath
1137 $selector = preg_replace ( '/\s+/', ' ', $selector ); // Normalize spaces
1138 $selectorParts = explode ( ' ', $selector );
1139
1140 $xpathQuery = '';
1141 foreach ( $selectorParts as $part ) {
1142 if (preg_match ( '/^([a-zA-Z0-9_-]+)?(\.[a-zA-Z0-9_-]+)?(#[a-zA-Z0-9_-]+)?$/', $part, $matches )) {
1143 $tag = ! empty ( $matches[1] ) ? $matches[1] : '*'; // If no tag, use '*'
1144 $class = ! empty ( $matches[2] ) ? substr ( $matches[2], 1 ) : ''; // Remove leading '.'
1145 $id = ! empty ( $matches[3] ) ? substr ( $matches[3], 1 ) : ''; // Remove leading '#'
1146
1147 $conditions = [ ];
1148 if ($class) {
1149 $conditions[] = "contains(concat(' ', normalize-space(@class), ' '), ' $class ')";
1150 }
1151 if ($id) {
1152 $conditions[] = "@id='$id'";
1153 }
1154
1155 $xpathQuery .= "//{$tag}" . (! empty ( $conditions ) ? "[" . implode ( " and ", $conditions ) . "]" : "");
1156 }
1157 }
1158
1159 if (! empty ( $xpathQuery )) {
1160 $xpathQueries[] = $xpathQuery;
1161 }
1162 }
1163
1164 // Execute all XPath queries and collect excluded nodes
1165 foreach ( $xpathQueries as $query ) {
1166 foreach ( $xpath->query ( $query ) as $excludedNode ) {
1167 $excludedNodes[] = $excludedNode;
1168 }
1169 }
1170 }
1171
1172 $textNodes = $xpath->query ( '//text()[not(ancestor::script) and not(ancestor::style) and normalize-space()]' );
1173 $caseInsensitive = ! empty ( $settings['serverside_translations_caseinsensitive'] );
1174 $matchQuotes = ! empty ( $settings['serverside_translations_matchquotes'] );
1175
1176 // Process each text node with translation replacements.
1177 foreach ( $textNodes as $textNode ) {
1178 if ($cssSelectorLeafnodesExcluded) {
1179 $skipNode = false;
1180 foreach ( $excludedNodes as $excludedNode ) {
1181 if ($excludedNode->isSameNode ( $textNode->parentNode )) {
1182 $skipNode = true;
1183 break;
1184 }
1185 }
1186 if ($skipNode)
1187 continue;
1188 }
1189
1190 $originalTextContent = $textNode->nodeValue;
1191 $textContent = $textNode->nodeValue;
1192 if ($matchQuotes) {
1193 $textContent = str_ireplace ( '"', "'", $textContent );
1194 }
1195 // Normalize once without trimming to preserve spaces between nodes
1196 $textContent = normalizeTextNoTrim($textContent);
1197
1198 // INCREMENTAL FIX: Use exact match only if incremental preserve new texts is enabled
1199 if ($incrementalEnabledPreserveNewTexts) {
1200 $textContent = gpt_incremental_translate_text($textContent, $incrementalContext);
1201 } else {
1202 // Normal pattern matching (standard behavior)
1203 foreach ( $translationsArray as $originalText => $translatedText ) {
1204 $originalText = normalizeText($originalText);
1205
1206 $prevTextContent = $textContent; // Save the current state
1207 if ($wrapExcludedWords) {
1208 // Use word boundaries to avoid sub-replacing inside already translated words
1209 $pattern = '/' . (preg_match('/^\w/u', trim($originalText)) ? '(?<!\w)' : '') . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
1210 $textContent = preg_replace($pattern, $translatedText, $textContent);
1211 } else {
1212 // Use word boundaries to avoid sub-replacing inside already translated words
1213 $pattern = '/' . (preg_match('/^\w/u', trim($originalText)) ? '(?<!\w)' : '') . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
1214 $textContent = preg_replace($pattern, $translatedText, $textContent);
1215 if ($textContent !== $prevTextContent) {
1216 break;
1217 }
1218 }
1219 }
1220 }
1221 $textNode->nodeValue = $textContent;
1222 }
1223
1224 // Replace page links in <a href="..."> tags based on translated aliases
1225 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
1226 $exclusions = $settings['rewrite_page_links_exclusions'] ?? '';
1227 $page_exclusions = $settings['page_exclusions'] ?? '';
1228 $linkNodes = $xpath->query('//a[@href]');
1229 foreach ($linkNodes as $linkNode) {
1230 $href = $linkNode->getAttribute('href');
1231
1232 // Check if link should be excluded from rewriting
1233 if (gptranslate_should_exclude_link($href, $exclusions, $page_exclusions)) {
1234 continue;
1235 }
1236
1237 // Decode href attribute (WordPress-style, KEEP trailing slash)
1238 $decodedHref = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
1239
1240 // Extract hash/fragment if present
1241 $hashFragment = '';
1242 if (strpos($decodedHref, '#') !== false) {
1243 $parts = explode('#', $decodedHref, 2);
1244 $decodedHref = $parts[0]; // URL without hash
1245 $hashFragment = '#' . $parts[1]; // Save the hash
1246 }
1247
1248 $queryString = '';
1249 if ($settings['ignore_querystring'] == 1 && strpos($decodedHref, '?') !== false) {
1250 $parts = explode('?', $decodedHref, 2);
1251 $decodedHref = $parts[0];
1252 $queryString = '?' . $parts[1];
1253 }
1254 $originalPathBeforeNormalization = $decodedHref;
1255
1256 $decodedHref = gpt_trailingslashit_url($decodedHref);
1257
1258 $translatedAlias = null;
1259
1260 // Cerca prima negli URL assoluti
1261 if (isset($translatedAliasesMap[$decodedHref])) {
1262 $translatedAlias = $translatedAliasesMap[$decodedHref];
1263 }
1264 // Poi cerca negli URL relativi
1265 elseif (isset($translatedAliasesRelativeMap[$decodedHref])) {
1266 $translatedAlias = $translatedAliasesRelativeMap[$decodedHref];
1267 }
1268
1269 // Se trovato un alias tradotto, sostituisci
1270 if ($translatedAlias) {
1271 // Aggiungi data-originalhref se non presente
1272 if (!$linkNode->hasAttribute('data-originalhref')) {
1273 $linkNode->setAttribute('data-originalhref', $originalPathBeforeNormalization . $queryString . $hashFragment);
1274 }
1275 $linkNode->setAttribute('href', $translatedAlias . $queryString . $hashFragment);
1276 }
1277 }
1278 }
1279
1280 // Replace form action URLs: add language prefix and apply translated aliases
1281 if (!empty($settings['rewrite_form_actions'])) {
1282 $siteUrl = trailingslashit(get_site_url());
1283 $siteHost = parse_url($siteUrl, PHP_URL_HOST);
1284 $knownLangs = (isset($settings['languages']) && is_array($settings['languages'])) ? array_map('strtolower', $settings['languages']) : [$originalLang, $translatedLang];
1285 $formNodes = $xpath->query('//form[@action]');
1286 foreach ($formNodes as $formNode) {
1287 $action = $formNode->getAttribute('action');
1288 $decodedAction = rawurldecode(html_entity_decode($action, ENT_QUOTES, 'UTF-8'));
1289
1290 // Skip non-rewritable actions
1291 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $decodedAction)) continue;
1292 if (preg_match('/\.php($|\?|\#)/i', $decodedAction)) continue;
1293 if (preg_match('/^https?:\/\//i', $decodedAction) && parse_url($decodedAction, PHP_URL_HOST) !== $siteHost) continue;
1294
1295 $queryString = '';
1296 if ($settings['ignore_querystring'] == 1 && strpos($decodedAction, '?') !== false) {
1297 $parts = explode('?', $decodedAction, 2);
1298 $decodedAction = $parts[0];
1299 $queryString = '?' . $parts[1];
1300 }
1301 $originalActionFull = $decodedAction . $queryString;
1302
1303 // Extract relative path
1304 $actionPath = $decodedAction;
1305 $isAbsolute = false;
1306 if (preg_match('/^https?:\/\//i', $actionPath)) {
1307 $isAbsolute = true;
1308 $actionPath = parse_url($actionPath, PHP_URL_PATH) ?: '/';
1309 }
1310
1311 // Add/replace language prefix
1312 $pathParts = explode('/', $actionPath);
1313 $langIndex = 1;
1314 if (isset($pathParts[$langIndex]) && $pathParts[$langIndex] === 'index.php') $langIndex = 2;
1315 if (!empty($settings['subfolder_installation'])) $langIndex = 2;
1316
1317 if (isset($pathParts[$langIndex]) && in_array(strtolower($pathParts[$langIndex]), $knownLangs)) {
1318 $pathParts[$langIndex] = $translatedLang;
1319 } else {
1320 array_splice($pathParts, $langIndex, 0, [$translatedLang]);
1321 }
1322
1323 $rebuiltPath = '/' . implode('/', array_filter($pathParts, function($p) { return $p !== ''; }));
1324 $rebuiltPath = gpt_trailingslashit_url($rebuiltPath);
1325 $computedAction = $isAbsolute ? (rtrim($siteUrl, '/') . $rebuiltPath) : $rebuiltPath;
1326 $computedAction = gpt_trailingslashit_url($computedAction);
1327
1328 // Try alias match
1329 $translatedAlias = null;
1330 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
1331 if (isset($translatedAliasesMap[$computedAction])) {
1332 $translatedAlias = $translatedAliasesMap[$computedAction];
1333 } elseif (isset($translatedAliasesRelativeMap[$rebuiltPath])) {
1334 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPath];
1335 } elseif (!$isAbsolute && !empty($settings['subfolder_installation'])) {
1336 // Fallback: form action is relative and includes subfolder path
1337 // Map keys strip the subfolder, so retry after stripping it
1338 $siteBasePath = rtrim(parse_url(rtrim($siteUrl, '/'), PHP_URL_PATH) ?: '', '/');
1339 if ($siteBasePath !== '' && strpos($rebuiltPath, $siteBasePath . '/') === 0) {
1340 $rebuiltPathNoSubfolder = substr($rebuiltPath, strlen($siteBasePath));
1341 if (isset($translatedAliasesRelativeMap[$rebuiltPathNoSubfolder])) {
1342 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPathNoSubfolder];
1343 }
1344 }
1345 }
1346 }
1347
1348 $finalAction = $translatedAlias ? ($translatedAlias . $queryString) : ($computedAction . $queryString);
1349
1350 if (!$formNode->hasAttribute('data-originalaction')) {
1351 $formNode->setAttribute('data-originalaction', $originalActionFull);
1352 }
1353 $formNode->setAttribute('action', $finalAction);
1354 }
1355 }
1356
1357 // Check if also images 'alt' are enabled to be translated
1358 if (! empty ( $settings['translate_altimages'] )) {
1359 $imgNodesAlt = $xpath->query ( '//img[@alt]' );
1360 foreach ( $imgNodesAlt as $imgNode ) {
1361 $altText = $imgNode->getAttribute ( 'alt' );
1362 if ($matchQuotes) {
1363 $altText = str_ireplace ( '"', "'", $altText );
1364 }
1365 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
1366 $originalText = normalizeText($originalText);
1367 $altText = normalizeText($altText);
1368
1369 $prevAltText = $altText;
1370 if ($caseInsensitive) {
1371 $altText = str_ireplace ( trim ( $originalText ), $translatedText, $altText );
1372 } else {
1373 $altText = str_replace ( trim ( $originalText ), $translatedText, $altText );
1374 }
1375 if ($altText !== $prevAltText) {
1376 break;
1377 }
1378 }
1379 $imgNode->setAttribute ( 'alt', $altText );
1380 }
1381
1382 $imgNodesTitle = $xpath->query ( '//img[@title]' );
1383 foreach ( $imgNodesTitle as $imgNode ) {
1384 $titleText = $imgNode->getAttribute ( 'title' );
1385 if ($matchQuotes) {
1386 $titleText = str_ireplace ( '"', "'", $titleText );
1387 }
1388 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
1389 $originalText = normalizeText($originalText);
1390 $titleText = normalizeText($titleText);
1391
1392 $prevTitleText = $titleText;
1393 if ($caseInsensitive) {
1394 $titleText = str_ireplace ( trim ( $originalText ), $translatedText, $titleText );
1395 } else {
1396 $titleText = str_replace ( trim ( $originalText ), $translatedText, $titleText );
1397 }
1398 if ($titleText !== $prevTitleText) {
1399 break;
1400 }
1401 }
1402 $imgNode->setAttribute ( 'title', $titleText );
1403 }
1404
1405 // Check if also images 'src' are enabled to be translated
1406 if (! empty ( $settings ['translate_srcimages'] )) {
1407 $imgNodesSrc = $xpath->query ( '//img[@src]' );
1408 foreach ( $imgNodesSrc as $imgNode ) {
1409 $srcValue = ltrim ( $imgNode->getAttribute ( 'src' ), '/' );
1410
1411 if ($matchQuotes) {
1412 $srcValue = str_ireplace ( '"', "'", $srcValue );
1413 }
1414
1415 $translatedSrcValue = null;
1416 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
1417 $originalText = normalizeText($originalText);
1418 $srcValue = normalizeText($srcValue);
1419
1420 $originalText = ltrim ( $originalText, '/' );
1421 if ($caseInsensitive) {
1422 if (strcasecmp ( trim ( $originalText ), $srcValue ) === 0) {
1423 $translatedSrcValue = $translatedText;
1424 break;
1425 }
1426 } else {
1427 if (trim ( $originalText ) === $srcValue) {
1428 $translatedSrcValue = $translatedText;
1429 break;
1430 }
1431 }
1432 }
1433
1434 // If a translation was found, replace src and srcset
1435 if ($translatedSrcValue !== null) {
1436 // Replace the src attribute
1437 $imgNode->setAttribute ( 'src', $translatedSrcValue );
1438
1439 // Check if srcset exists and replace it with the translated src value
1440 if ($imgNode->hasAttribute ( 'srcset' )) {
1441 $imgNode->setAttribute ( 'srcset', $translatedSrcValue );
1442 }
1443 }
1444 }
1445 }
1446
1447 // Check if also iframes 'src' are enabled to be translated
1448 if (! empty ( $settings ['translate_srciframes'] )) {
1449 $iframeNodesSrc = $xpath->query ( '//iframe[@src]' );
1450 foreach ( $iframeNodesSrc as $iframeNode ) {
1451 $srcValue = ltrim ( $iframeNode->getAttribute ( 'src' ), '/' );
1452
1453 if ($matchQuotes) {
1454 $srcValue = str_ireplace ( '"', "'", $srcValue );
1455 }
1456
1457 $translatedSrcValue = null;
1458 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
1459 $originalText = normalizeText($originalText);
1460 $srcValue = normalizeText($srcValue);
1461
1462 $originalText = ltrim ( $originalText, '/' );
1463 if ($caseInsensitive) {
1464 if (strcasecmp ( trim ( $originalText ), $srcValue ) === 0) {
1465 $translatedSrcValue = $translatedText;
1466 break;
1467 }
1468 } else {
1469 if (trim ( $originalText ) === $srcValue) {
1470 $translatedSrcValue = $translatedText;
1471 break;
1472 }
1473 }
1474 }
1475
1476 if ($translatedSrcValue !== null) {
1477 $iframeNode->setAttribute ( 'src', $translatedSrcValue );
1478 }
1479 }
1480 }
1481
1482 // Check if also videos 'src' are enabled to be translated
1483 if (! empty ( $settings ['translate_srcvideos'] )) {
1484 $videoNodesSrc = $xpath->query ( '//video[@src] | //video//source[@src]' );
1485 foreach ( $videoNodesSrc as $videoNode ) {
1486 $srcValue = ltrim ( $videoNode->getAttribute ( 'src' ), '/' );
1487
1488 if ($matchQuotes) {
1489 $srcValue = str_ireplace ( '"', "'", $srcValue );
1490 }
1491
1492 $translatedSrcValue = null;
1493 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
1494 $originalText = normalizeText($originalText);
1495 $srcValue = normalizeText($srcValue);
1496
1497 $originalText = ltrim ( $originalText, '/' );
1498 if ($caseInsensitive) {
1499 if (strcasecmp ( trim ( $originalText ), $srcValue ) === 0) {
1500 $translatedSrcValue = $translatedText;
1501 break;
1502 }
1503 } else {
1504 if (trim ( $originalText ) === $srcValue) {
1505 $translatedSrcValue = $translatedText;
1506 break;
1507 }
1508 }
1509 }
1510
1511 if ($translatedSrcValue !== null) {
1512 $videoNode->setAttribute ( 'src', $translatedSrcValue );
1513 }
1514 }
1515 }
1516 }
1517
1518 // Translate iframe locale parameter (e.g. Stripe payment forms)
1519 if (! empty ( $settings ['translate_iframe_locale'] )) {
1520 $currentLangCode = strtolower(explode('-', $currentLanguage)[0]);
1521 $iframeNodes = $xpath->query ( '//iframe[@src]' );
1522 foreach ( $iframeNodes as $iframeNode ) {
1523 $src = $iframeNode->getAttribute ( 'src' );
1524 if (strpos ( $src, 'locale=' ) !== false) {
1525 $translatedSrc = preg_replace ( '/locale=[a-z]{2}/i', 'locale=' . $currentLangCode, $src );
1526 $iframeNode->setAttribute ( 'src', $translatedSrc );
1527 }
1528 }
1529 }
1530
1531 // Translate meta descriptions (standard, Open Graph, Twitter, Dublin Core)
1532 $metaDescriptions = $xpath->query(
1533 '//meta[
1534 (@name="description")
1535 or (@name="twitter:description")
1536 or (@property="og:description")
1537 or (@name="dc.description")
1538 ]'
1539 );
1540 foreach ($metaDescriptions as $meta) {
1541 $contentValue = $meta->getAttribute('content');
1542
1543 if ($matchQuotes) {
1544 $contentValue = str_ireplace('"', "'", $contentValue);
1545 }
1546
1547 foreach ($altTranslationsArray as $originalText => $translatedText) {
1548 $prev = $contentValue;
1549 if ($caseInsensitive) {
1550 $contentValue = str_ireplace(trim($originalText), $translatedText, $contentValue);
1551 } else {
1552 $contentValue = str_replace(trim($originalText), $translatedText, $contentValue);
1553 }
1554 if ($contentValue !== $prev) break;
1555 }
1556
1557 $meta->setAttribute('content', $contentValue);
1558 }
1559
1560 $ogTitleNodes = $xpath->query('//meta[@property="og:title"]');
1561 if ($ogTitleNodes->length > 0) {
1562 // Find the <title> tag to get the translated title
1563 $titleNodes = $xpath->query('//title');
1564
1565 if ($titleNodes->length > 0) {
1566 $titleNode = $titleNodes->item(0);
1567 $translatedTitle = $titleNode->textContent;
1568
1569 // Set og:title with the same value as <title>
1570 foreach ($ogTitleNodes as $ogTitleNode) {
1571 $ogTitleNode->setAttribute('content', trim($translatedTitle));
1572 }
1573
1574 // Set twitter:title with the same value as <title>
1575 $twitterTitleNodes = $xpath->query('//meta[@name="twitter:title"]');
1576 foreach ($twitterTitleNodes as $twitterTitleNode) {
1577 $twitterTitleNode->setAttribute('content', trim($translatedTitle));
1578 }
1579
1580 // Set Dublin Core (SEOPress) dc:title with the same value as <title>
1581 $dcTitleNodes = $xpath->query('//meta[@name="dc.title"]');
1582 foreach ($dcTitleNodes as $dcTitleNode) {
1583 $dcTitleNode->setAttribute('content', trim($translatedTitle));
1584 }
1585 }
1586 }
1587
1588 // Optionally, add an attribute to the <body> tag to signal that translations were applied.
1589 $bodyTag = $doc->getElementsByTagName ( 'body' )->item ( 0 );
1590 if ($bodyTag) {
1591 $bodyTag->setAttribute ( 'data-gptranslateskip', '1' );
1592 $bodyTag->setAttribute ( 'data-gptranslateoriginalalias', htmlspecialchars($row['pagelink'], ENT_QUOTES, 'UTF-8') );
1593 }
1594
1595 // Save the modified HTML and strip the XML processing instruction
1596 // that was injected as a UTF-8 encoding hint for DOMDocument.
1597 $html = $doc->saveHTML ();
1598 $html = preg_replace ( '/<\?xml[^>]*>\s*/i', '', $html );
1599
1600 // RESTORE protected script/style content (if protection was enabled)
1601 if (!empty($settings['protect_script_style'])) {
1602 // Restore scripts from meta markers
1603 $html = preg_replace_callback(
1604 '/<meta\s+data-gptranslate-script="([^"]+)"[^>]*>/i',
1605 function ($matches) use ($protectedScriptsStyles) {
1606 $placeholder = $matches[1];
1607 return isset($protectedScriptsStyles[$placeholder]) ? $protectedScriptsStyles[$placeholder] : $matches[0];
1608 },
1609 $html
1610 );
1611
1612 // Restore styles from meta markers
1613 $html = preg_replace_callback(
1614 '/<meta\s+data-gptranslate-style="([^"]+)"[^>]*>/i',
1615 function ($matches) use ($protectedScriptsStyles) {
1616 $placeholder = $matches[1];
1617 return isset($protectedScriptsStyles[$placeholder]) ? $protectedScriptsStyles[$placeholder] : $matches[0];
1618 },
1619 $html
1620 );
1621 }
1622
1623 // Decode HTML entities back into UTF-8 characters.
1624 $html = html_entity_decode ( $html, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
1625
1626 } catch ( Exception $e ) {
1627 // Handle exception if needed
1628 }
1629 } elseif ($settings['serverside_translations_method'] == 'simplehtmldom') {
1630 require_once plugin_dir_path(__FILE__) . 'simplehtmldom.php';
1631
1632 // Helper: checks if $child is inside (or is) $parent.
1633 function nodeIsInsideExcluded($child, $excludedNodes) {
1634 while ($child !== null) {
1635 foreach ($excludedNodes as $ex) {
1636 if ($child === $ex) {
1637 return true;
1638 }
1639 }
1640 $child = $child->parent;
1641 }
1642 return false;
1643 }
1644
1645 // Helper: checks if $node is inside (or is) any of the specified tags.
1646 function nodeIsInsideTags($node, $tagNames) {
1647 $tagNames = array_map('strtolower', $tagNames);
1648 while ($node !== null) {
1649 $tag = strtolower($node->tag ?? '');
1650 if (in_array($tag, $tagNames, true)) {
1651 return true;
1652 }
1653 $node = $node->parent;
1654 }
1655 return false;
1656 }
1657
1658 // Recursive function to process all text nodes.
1659 function processTextNodes($node, $excludedNodes, $translationsArray, $caseInsensitive, $matchQuotes, $incrementalEnabledPreserveNewTexts, $incrementalContext) {
1660 if ($node->tag === 'text') {
1661 if (! nodeIsInsideExcluded($node, $excludedNodes) && ! nodeIsInsideTags($node, ['script', 'style', 'noscript']) && trim($node->innertext) && $node->innertext != "\t") {
1662 $text = $node->innertext;
1663 if ($matchQuotes) {
1664 $text = str_ireplace('"', "'", $text);
1665 }
1666
1667 // Normalize once to avoid stripping spaces added by replacements
1668 $text = normalizeTextNoTrim($text);
1669
1670 // INCREMENTAL FIX: Use exact match only if incremental preserve new texts is enabled
1671 if ($incrementalEnabledPreserveNewTexts) {
1672 $text = gpt_incremental_translate_text($text, $incrementalContext);
1673 } else {
1674 // Normal pattern matching (standard behavior)
1675 // Apply translations without overriding longer translations
1676 $processedParts = [];
1677 foreach ($translationsArray as $originalText => $translatedText) {
1678 $originalText = normalizeText($originalText);
1679
1680 if ($caseInsensitive) {
1681 $text = preg_replace_callback(
1682 '/' . (preg_match('/^\w/u', trim($originalText)) ? '(?<!\w)' : '') . preg_quote(trim($originalText), '/') . '(?!\w)/ui',
1683 function ($matches) use ($translatedText, &$processedParts) {
1684 if (in_array($matches[0], $processedParts, true)) {
1685 return $matches[0];
1686 }
1687 $processedParts[] = $translatedText;
1688 return $translatedText;
1689 },
1690 $text
1691 );
1692 } else {
1693 $text = preg_replace_callback(
1694 '/' . (preg_match('/^\w/u', trim($originalText)) ? '(?<!\w)' : '') . preg_quote(trim($originalText), '/') . '(?!\w)/',
1695 function ($matches) use ($translatedText, &$processedParts) {
1696 if (in_array($matches[0], $processedParts, true)) {
1697 return $matches[0];
1698 }
1699 $processedParts[] = $translatedText;
1700 return $translatedText;
1701 },
1702 $text
1703 );
1704 }
1705 }
1706 }
1707 $node->innertext = $text;
1708 }
1709 } else {
1710 $tagLower = strtolower($node->tag ?? '');
1711 if (in_array($tagLower, ['script', 'style'])) {
1712 return;
1713 }
1714 if (isset($node->nodes) && is_array($node->nodes)) {
1715 foreach ($node->nodes as $child) {
1716 processTextNodes($child, $excludedNodes, $translationsArray, $caseInsensitive, $matchQuotes, $incrementalEnabledPreserveNewTexts, $incrementalContext);
1717 }
1718 }
1719 }
1720 }
1721
1722 // Main processing code.
1723 try {
1724 $translationsArray = json_decode($row['translations'], true) ?? [];
1725 $altTranslationsArray = json_decode($row['alt_translations'] ?? '', true) ?: [];
1726
1727 // Sort translations by descending key length
1728 uksort($translationsArray, function ($a, $b) {
1729 return strlen($b) - strlen($a);
1730 });
1731
1732 // Sort alt translations by descending key length
1733 uksort($altTranslationsArray, function ($a, $b) {
1734 return strlen($b) - strlen($a);
1735 });
1736
1737 // Flatten formatting tags regex
1738 if($flattenInnerFormattingTags) {
1739 // Flatten formatting tags in the body to improve matching if enabled
1740 function gp_flatten_inner_formatting_tags_simplehtmldom($htmlString, $tagsToRemove ) {
1741 // Complete body parsing
1742 $doc = gptranslate_simplehtmldom_str_get_html ( $htmlString );
1743 if (! $doc)
1744 return $htmlString;
1745
1746 $selector = implode ( ',', $tagsToRemove );
1747
1748 $found = $doc->find($selector);
1749 if (!empty($found)) {
1750 for ($i = count($found) - 1; $i >= 0; $i--) {
1751 $n = $found[$i];
1752 // Preserva elementi vuoti (es. <i> FontAwesome/icon fonts)
1753 if (trim(str_replace(['&nbsp;', "\xc2\xa0", "\xa0"], '', $n->plaintext)) === '') {
1754 continue;
1755 }
1756 // Preserva elementi che contengono form elements (select, input, textarea, button, ecc.)
1757 $interactiveTags = ['select', 'input', 'textarea', 'button', 'option', 'optgroup', 'fieldset', 'datalist', 'output', 'label', 'form', 'img', 'video', 'audio', 'canvas', 'svg', 'iframe'];
1758 $hasInteractive = false;
1759 foreach ($interactiveTags as $iTag) {
1760 if (!empty($n->find($iTag))) {
1761 $hasInteractive = true;
1762 break;
1763 }
1764 }
1765 if ($hasInteractive) {
1766 continue;
1767 }
1768 $n->outertext = $n->plaintext;
1769 }
1770 }
1771
1772 $out = $doc->save();
1773
1774 if (method_exists($doc, 'clear')) $doc->clear();
1775 unset($doc);
1776
1777 return $out;
1778 }
1779 $html = gp_flatten_inner_formatting_tags_simplehtmldom($html, $flattenInnerFormattingTagsToRemove);
1780 }
1781
1782 // PROTECT script/style content before SimpleHtmlDom parsing (if option enabled)
1783 $protectedScriptsStyles = [];
1784 if (!empty($settings['protect_script_style'])) {
1785 $protectCounter = 0;
1786 $html = preg_replace_callback(
1787 '/<script[^>]*>.*?<\/script>/is',
1788 function ($matches) use (&$protectedScriptsStyles, &$protectCounter) {
1789 $placeholder = "___GPTRANSLATE_SCRIPT_" . ($protectCounter++) . "___";
1790 $protectedScriptsStyles[$placeholder] = $matches[0];
1791 return $placeholder;
1792 },
1793 $html
1794 );
1795 $html = preg_replace_callback(
1796 '/<style[^>]*>.*?<\/style>/is',
1797 function ($matches) use (&$protectedScriptsStyles, &$protectCounter) {
1798 $placeholder = "___GPTRANSLATE_STYLE_" . ($protectCounter++) . "___";
1799 $protectedScriptsStyles[$placeholder] = $matches[0];
1800 return $placeholder;
1801 },
1802 $html
1803 );
1804 }
1805 $htmlObj = gptranslate_simplehtmldom_str_get_html($html);
1806
1807 $cssSelectorLeafnodesExcluded = str_ireplace(
1808 '"',
1809 '',
1810 trim(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_serverside_leafnodes_excluded'] ?? ''))), ',')
1811 );
1812
1813 $excludedNodes = [];
1814 if (! empty($cssSelectorLeafnodesExcluded)) {
1815 $selectors = explode(',', $cssSelectorLeafnodesExcluded);
1816 foreach ($selectors as $selector) {
1817 $selector = trim($selector);
1818 if (! empty($selector)) {
1819 $foundNodes = $htmlObj->find($selector);
1820 foreach ($foundNodes as $node) {
1821 $excludedNodes[] = $node;
1822 }
1823 }
1824 }
1825 }
1826
1827 $caseInsensitive = ! empty($settings['serverside_translations_caseinsensitive']);
1828 $matchQuotes = ! empty($settings['serverside_translations_matchquotes']);
1829
1830 processTextNodes($htmlObj, $excludedNodes, $translationsArray, $caseInsensitive, $matchQuotes, $incrementalEnabledPreserveNewTexts, $incrementalContext);
1831
1832 // Replace page links in <a href="..."> tags based on translated aliases
1833 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
1834 $exclusions = $settings['rewrite_page_links_exclusions'] ?? '';
1835 $page_exclusions = $settings['page_exclusions'] ?? '';
1836 foreach ($htmlObj->find('a[href]') as $linkNode) {
1837 $href = $linkNode->href;
1838
1839 // Check if link should be excluded from rewriting
1840 if (gptranslate_should_exclude_link($href, $exclusions, $page_exclusions)) {
1841 continue;
1842 }
1843
1844 // Decode href attribute (WordPress-style, KEEP trailing slash)
1845 $decodedHref = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
1846
1847 // Extract hash/fragment if present
1848 $hashFragment = '';
1849 if (strpos($decodedHref, '#') !== false) {
1850 $parts = explode('#', $decodedHref, 2);
1851 $decodedHref = $parts[0]; // URL without hash
1852 $hashFragment = '#' . $parts[1]; // Save the hash
1853 }
1854
1855 // Extract query string if ignore_querystring is enabled
1856 $queryString = '';
1857 if ($settings['ignore_querystring'] == 1 && strpos($decodedHref, '?') !== false) {
1858 $parts = explode('?', $decodedHref, 2);
1859 $decodedHref = $parts[0];
1860 $queryString = '?' . $parts[1];
1861 }
1862 $originalPathBeforeNormalization = $decodedHref;
1863
1864 $decodedHref = gpt_trailingslashit_url($decodedHref);
1865
1866 $translatedAlias = null;
1867
1868 // Cerca prima negli URL assoluti
1869 if (isset($translatedAliasesMap[$decodedHref])) {
1870 $translatedAlias = $translatedAliasesMap[$decodedHref];
1871 }
1872 // Poi cerca negli URL relativi
1873 elseif (isset($translatedAliasesRelativeMap[$decodedHref])) {
1874 $translatedAlias = $translatedAliasesRelativeMap[$decodedHref];
1875 }
1876
1877 // Se trovato un alias tradotto, sostituisci
1878 if ($translatedAlias) {
1879 // Aggiungi data-originalhref se non presente
1880 if (!isset($linkNode->{'data-originalhref'})) {
1881 $linkNode->{'data-originalhref'} = $originalPathBeforeNormalization . $queryString . $hashFragment;
1882 }
1883 $linkNode->href = $translatedAlias . $queryString . $hashFragment;
1884 }
1885 }
1886 }
1887
1888 // Replace form action URLs: add language prefix and apply translated aliases
1889 if (!empty($settings['rewrite_form_actions'])) {
1890 $siteUrl = trailingslashit(get_site_url());
1891 $siteHost = parse_url($siteUrl, PHP_URL_HOST);
1892 $knownLangs = (isset($settings['languages']) && is_array($settings['languages'])) ? array_map('strtolower', $settings['languages']) : [$originalLang, $translatedLang];
1893 foreach ($htmlObj->find('form[action]') as $formNode) {
1894 $action = $formNode->action;
1895 $decodedAction = rawurldecode(html_entity_decode($action, ENT_QUOTES, 'UTF-8'));
1896
1897 // Skip non-rewritable actions
1898 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $decodedAction)) continue;
1899 if (preg_match('/\.php($|\?|\#)/i', $decodedAction)) continue;
1900 if (preg_match('/^https?:\/\//i', $decodedAction) && parse_url($decodedAction, PHP_URL_HOST) !== $siteHost) continue;
1901
1902 $queryString = '';
1903 if ($settings['ignore_querystring'] == 1 && strpos($decodedAction, '?') !== false) {
1904 $parts = explode('?', $decodedAction, 2);
1905 $decodedAction = $parts[0];
1906 $queryString = '?' . $parts[1];
1907 }
1908 $originalActionFull = $decodedAction . $queryString;
1909
1910 // Extract relative path
1911 $actionPath = $decodedAction;
1912 $isAbsolute = false;
1913 if (preg_match('/^https?:\/\//i', $actionPath)) {
1914 $isAbsolute = true;
1915 $actionPath = parse_url($actionPath, PHP_URL_PATH) ?: '/';
1916 }
1917
1918 // Add/replace language prefix
1919 $pathParts = explode('/', $actionPath);
1920 $langIndex = 1;
1921 if (isset($pathParts[$langIndex]) && $pathParts[$langIndex] === 'index.php') $langIndex = 2;
1922 if (!empty($settings['subfolder_installation'])) $langIndex = 2;
1923
1924 if (isset($pathParts[$langIndex]) && in_array(strtolower($pathParts[$langIndex]), $knownLangs)) {
1925 $pathParts[$langIndex] = $translatedLang;
1926 } else {
1927 array_splice($pathParts, $langIndex, 0, [$translatedLang]);
1928 }
1929
1930 $rebuiltPath = '/' . implode('/', array_filter($pathParts, function($p) { return $p !== ''; }));
1931 $rebuiltPath = gpt_trailingslashit_url($rebuiltPath);
1932 $computedAction = $isAbsolute ? (rtrim($siteUrl, '/') . $rebuiltPath) : $rebuiltPath;
1933 $computedAction = gpt_trailingslashit_url($computedAction);
1934
1935 // Try alias match
1936 $translatedAlias = null;
1937 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
1938 if (isset($translatedAliasesMap[$computedAction])) {
1939 $translatedAlias = $translatedAliasesMap[$computedAction];
1940 } elseif (isset($translatedAliasesRelativeMap[$rebuiltPath])) {
1941 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPath];
1942 } elseif (!$isAbsolute && !empty($settings['subfolder_installation'])) {
1943 // Fallback: form action is relative and includes subfolder path
1944 // Map keys strip the subfolder, so retry after stripping it
1945 $siteBasePath = rtrim(parse_url(rtrim($siteUrl, '/'), PHP_URL_PATH) ?: '', '/');
1946 if ($siteBasePath !== '' && strpos($rebuiltPath, $siteBasePath . '/') === 0) {
1947 $rebuiltPathNoSubfolder = substr($rebuiltPath, strlen($siteBasePath));
1948 if (isset($translatedAliasesRelativeMap[$rebuiltPathNoSubfolder])) {
1949 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPathNoSubfolder];
1950 }
1951 }
1952 }
1953 }
1954
1955 $finalAction = $translatedAlias ? ($translatedAlias . $queryString) : ($computedAction . $queryString);
1956
1957 if (!isset($formNode->{'data-originalaction'})) {
1958 $formNode->{'data-originalaction'} = $originalActionFull;
1959 }
1960 $formNode->action = $finalAction;
1961 }
1962 }
1963
1964 // Check if also images 'alt' are enabled to be translated
1965 if (! empty($settings['translate_altimages'])) {
1966 foreach ($htmlObj->find('img[alt]') as $imgNode) {
1967 $altText = $imgNode->alt;
1968 if ($matchQuotes) {
1969 $altText = str_ireplace('"', "'", $altText);
1970 }
1971 foreach ($altTranslationsArray as $originalText => $translatedText) {
1972 $originalText = normalizeText($originalText);
1973 $altText = normalizeText($altText);
1974
1975 $prevAltText = $altText;
1976 if ($caseInsensitive) {
1977 $altText = str_ireplace(trim($originalText), $translatedText, $altText);
1978 } else {
1979 $altText = str_replace(trim($originalText), $translatedText, $altText);
1980 }
1981 if ($altText !== $prevAltText) {
1982 break;
1983 }
1984 }
1985 $imgNode->alt = $altText;
1986 }
1987
1988 foreach ($htmlObj->find('img[title]') as $imgNode) {
1989 $titleText = $imgNode->title;
1990 if ($matchQuotes) {
1991 $titleText = str_ireplace('"', "'", $titleText);
1992 }
1993 foreach ($altTranslationsArray as $originalText => $translatedText) {
1994 $originalText = normalizeText($originalText);
1995 $titleText = normalizeText($titleText);
1996
1997 $prevTitleText = $titleText;
1998 if ($caseInsensitive) {
1999 $titleText = str_ireplace(trim($originalText), $translatedText, $titleText);
2000 } else {
2001 $titleText = str_replace(trim($originalText), $translatedText, $titleText);
2002 }
2003 if ($titleText !== $prevTitleText) {
2004 break;
2005 }
2006 }
2007 $imgNode->title = $titleText;
2008 }
2009
2010 // Check if also images 'src' are enabled to be translated
2011 if (! empty($settings['translate_srcimages'])) {
2012 foreach ($htmlObj->find('img[src]') as $imgNode) {
2013 $srcValue = ltrim($imgNode->src, '/');
2014
2015 if ($matchQuotes) {
2016 $srcValue = str_ireplace('"', "'", $srcValue);
2017 }
2018
2019 $translatedSrcValue = null;
2020 foreach ($altTranslationsArray as $originalText => $translatedText) {
2021 $originalText = normalizeText($originalText);
2022 $srcValue = normalizeText($srcValue);
2023
2024 $originalText = ltrim($originalText, '/');
2025 if ($caseInsensitive) {
2026 if (strcasecmp(trim($originalText), $srcValue) === 0) {
2027 $translatedSrcValue = $translatedText;
2028 break;
2029 }
2030 } else {
2031 if (trim($originalText) === $srcValue) {
2032 $translatedSrcValue = $translatedText;
2033 break;
2034 }
2035 }
2036 }
2037
2038 // If a translation was found, replace src and srcset
2039 if ($translatedSrcValue !== null) {
2040 // Replace the src attribute
2041 $imgNode->src = $translatedSrcValue;
2042
2043 // Check if srcset exists and replace it with the translated src value
2044 if (isset($imgNode->srcset)) {
2045 $imgNode->srcset = $translatedSrcValue;
2046 }
2047 }
2048 }
2049 }
2050
2051 // Check if also iframes 'src' are enabled to be translated
2052 if (! empty($settings['translate_srciframes'])) {
2053 foreach ($htmlObj->find('iframe[src]') as $iframeNode) {
2054 $srcValue = ltrim($iframeNode->src, '/');
2055
2056 if ($matchQuotes) {
2057 $srcValue = str_ireplace('"', "'", $srcValue);
2058 }
2059
2060 $translatedSrcValue = null;
2061 foreach ($altTranslationsArray as $originalText => $translatedText) {
2062 $originalText = normalizeText($originalText);
2063 $srcValue = normalizeText($srcValue);
2064
2065 $originalText = ltrim($originalText, '/');
2066 if ($caseInsensitive) {
2067 if (strcasecmp(trim($originalText), $srcValue) === 0) {
2068 $translatedSrcValue = $translatedText;
2069 break;
2070 }
2071 } else {
2072 if (trim($originalText) === $srcValue) {
2073 $translatedSrcValue = $translatedText;
2074 break;
2075 }
2076 }
2077 }
2078
2079 if ($translatedSrcValue !== null) {
2080 $iframeNode->src = $translatedSrcValue;
2081 }
2082 }
2083 }
2084
2085 // Check if also videos 'src' are enabled to be translated
2086 if (! empty($settings['translate_srcvideos'])) {
2087 foreach ($htmlObj->find('video[src], video source[src]') as $videoNode) {
2088 $srcValue = ltrim($videoNode->src, '/');
2089
2090 if ($matchQuotes) {
2091 $srcValue = str_ireplace('"', "'", $srcValue);
2092 }
2093
2094 $translatedSrcValue = null;
2095 foreach ($altTranslationsArray as $originalText => $translatedText) {
2096 $originalText = normalizeText($originalText);
2097 $srcValue = normalizeText($srcValue);
2098
2099 $originalText = ltrim($originalText, '/');
2100 if ($caseInsensitive) {
2101 if (strcasecmp(trim($originalText), $srcValue) === 0) {
2102 $translatedSrcValue = $translatedText;
2103 break;
2104 }
2105 } else {
2106 if (trim($originalText) === $srcValue) {
2107 $translatedSrcValue = $translatedText;
2108 break;
2109 }
2110 }
2111 }
2112
2113 if ($translatedSrcValue !== null) {
2114 $videoNode->src = $translatedSrcValue;
2115 }
2116 }
2117 }
2118 }
2119
2120 // Translate iframe locale parameter (e.g. Stripe payment forms)
2121 if (! empty($settings['translate_iframe_locale'])) {
2122 $currentLangCode = strtolower(explode('-', $currentLanguage)[0]);
2123 foreach ($htmlObj->find('iframe[src]') as $iframeNode) {
2124 $src = $iframeNode->src;
2125 if (strpos($src, 'locale=') !== false) {
2126 $iframeNode->src = preg_replace('/locale=[a-z]{2}/i', 'locale=' . $currentLangCode, $src);
2127 }
2128 }
2129 }
2130
2131 // Translate <meta name="description">
2132 foreach ($htmlObj->find('meta[name=description], meta[name=twitter:description], meta[property=og:description], meta[name=dc.description]') as $metaNode) {
2133 $contentValue = $metaNode->content;
2134
2135 // DECODIFICA HTML entities PRIMA del confronto
2136 $contentValue = html_entity_decode($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
2137
2138 if ($matchQuotes) {
2139 $contentValue = str_ireplace('"', "'", $contentValue);
2140 }
2141
2142 foreach ($altTranslationsArray as $originalText => $translatedText) {
2143 $prev = $contentValue;
2144 if ($caseInsensitive) {
2145 $contentValue = str_ireplace(trim($originalText), $translatedText, $contentValue);
2146 } else {
2147 $contentValue = str_replace(trim($originalText), $translatedText, $contentValue);
2148 }
2149 if ($contentValue !== $prev) break;
2150 }
2151
2152 // RI-ENCODIFICA per l'HTML output (sicurezza)
2153 $contentValue = htmlspecialchars($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
2154
2155 $metaNode->content = $contentValue;
2156 }
2157
2158 foreach ($htmlObj->find('meta[property=og:title]') as $metaOgTitleNode) {
2159 // Find the <title> tag to get the translated title
2160 $titleNode = $htmlObj->find('title', 0);
2161
2162 if ($titleNode) {
2163 // Get the translated title text
2164 $translatedTitle = $titleNode->plaintext;
2165
2166 // Set og:title with the same value as <title>
2167 $metaOgTitleNode->content = trim($translatedTitle);
2168 }
2169 }
2170
2171 // Translate <meta name="twitter:title"> with the same value as <title> tag
2172 foreach ($htmlObj->find('meta[name=twitter:title]') as $metaTwitterTitleNode) {
2173 // Find the <title> tag to get the translated title
2174 $titleNode = $htmlObj->find('title', 0);
2175
2176 if ($titleNode) {
2177 // Get the translated title text
2178 $translatedTitle = $titleNode->plaintext;
2179
2180 // Set twitter:title with the same value as <title>
2181 $metaTwitterTitleNode->content = trim($translatedTitle);
2182 }
2183 }
2184
2185 // Translate <meta name="dc:title"> (Dublin Core - SEOPress) with the same value as <title> tag
2186 foreach ($htmlObj->find('meta[name=dc.title]') as $metaDcTitleNode) {
2187 // Find the <title> tag to get the translated title
2188 $titleNode = $htmlObj->find('title', 0);
2189
2190 if ($titleNode) {
2191 // Get the translated title text
2192 $translatedTitle = $titleNode->plaintext;
2193
2194 // Set dc:title with the same value as <title>
2195 $metaDcTitleNode->content = trim($translatedTitle);
2196 }
2197 }
2198
2199 if ($bodyElement = $htmlObj->find('body', 0)) {
2200 $bodyElement->setAttribute('data-gptranslateskip', '1');
2201 $bodyElement->setAttribute ( 'data-gptranslateoriginalalias', htmlspecialchars($row['pagelink'], ENT_QUOTES, 'UTF-8') );
2202 }
2203
2204 $modifiedHtml = $htmlObj->save();
2205 $modifiedHtml = html_entity_decode($modifiedHtml, ENT_QUOTES | ENT_HTML5, 'UTF-8');
2206
2207 // RESTORE protected script/style content (if protection was enabled)
2208 if (!empty($settings['protect_script_style'])) {
2209 foreach ($protectedScriptsStyles as $placeholder => $originalContent) {
2210 $modifiedHtml = str_replace($placeholder, $originalContent, $modifiedHtml);
2211 }
2212 }
2213 $html = $modifiedHtml;
2214 } catch (Exception $e) {
2215 // Handle exceptions as needed
2216 }
2217 } elseif ($settings['serverside_translations_method'] == 'strireplace') {
2218 // Solution 3: simplest approach, unconditional str_ireplace that could cause unintentional replacements
2219 try {
2220 $translationsArray = json_decode($row['translations'], true) ?? [];
2221 $altTranslationsArray = json_decode($row['alt_translations'] ?? '', true) ?: [];
2222
2223 // Do body page translations replacements
2224 uksort($translationsArray, function ($a, $b) {
2225 return strlen($b) - strlen($a);
2226 });
2227
2228 $caseInsensitive = !empty($settings['serverside_translations_caseinsensitive']);
2229 $matchQuotes = !empty($settings['serverside_translations_matchquotes']);
2230
2231 foreach ($translationsArray as $originalText => $translatedText) {
2232 $originalText = normalizeText($originalText);
2233 $html = normalizeText($html);
2234
2235 if ($caseInsensitive) {
2236 $html = str_ireplace(trim($originalText), $translatedText, $html);
2237
2238 // Check also if both single quotes or double quotes should be checked to replace
2239 if ($matchQuotes && strpos($originalText, "'") !== false) {
2240 $originalTextAlt = str_ireplace("'", '"', $originalText);
2241 $html = str_ireplace(trim($originalTextAlt), $translatedText, $html);
2242 }
2243 } else {
2244 $html = str_replace(trim($originalText), $translatedText, $html);
2245
2246 if ($matchQuotes && strpos($originalText, "'") !== false) {
2247 $originalTextAlt = str_replace("'", '"', $originalText);
2248 $html = str_replace(trim($originalTextAlt), $translatedText, $html);
2249 }
2250 }
2251 }
2252
2253 // Check if also images 'alt' are enabled to be translated
2254 if (!empty($settings['translate_altimages'])) {
2255 foreach ($altTranslationsArray as $originalAlt => $translatedAlt) {
2256 if ($matchQuotes) {
2257 $originalAlt = str_ireplace('"', "'", $originalAlt);
2258 }
2259 if ($caseInsensitive) {
2260 $html = str_ireplace('alt="' . trim($originalAlt) . '"', 'alt="' . $translatedAlt . '"', $html);
2261 $html = str_ireplace("alt='" . trim($originalAlt) . "'", "alt='" . $translatedAlt . "'", $html);
2262 } else {
2263 $html = str_replace('alt="' . trim($originalAlt) . '"', 'alt="' . $translatedAlt . '"', $html);
2264 $html = str_replace("alt='" . trim($originalAlt) . "'", "alt='" . $translatedAlt . "'", $html);
2265 }
2266 }
2267 }
2268
2269 $html = str_ireplace('<body', '<body data-gptranslateskip="1" data-gptranslateoriginalalias="' . $row['pagelink'] . '"', $html);
2270
2271 } catch (Exception $e) {
2272 // Handle exceptions as needed
2273 }
2274 }
2275
2276 return $html;
2277 } );
2278 } );
2279