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