PluginProbe ʕ •ᴥ•ʔ
GPTranslate – Multilingual AI Translation for WordPress: Automatically Translate Websites / 2.28
GPTranslate – Multilingual AI Translation for WordPress: Automatically Translate Websites v2.28
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 3 months ago flags 3 months ago includes 3 months ago language 3 months ago ajax-handler.php 3 months ago gptranslate.php 3 months ago multilang-routing.php 3 months ago readme.txt 3 months ago serverside-translations.php 3 months ago settings.php 3 months ago simplehtmldom.php 3 months ago uninstall.php 3 months ago
serverside-translations.php
1628 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 ob_start ( function ($html) use ($uri, $settings) {
40 global $wpdb;
41
42 $originalLang = $settings ['language'] ?? '';
43
44 if (! defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
45 return $html;
46 }
47
48 $translatedLang = GPTRANSLATE_CURRENT_LANG;
49
50 if (empty ( $originalLang ) || $originalLang === $translatedLang)
51 return $html;
52
53 if ($settings['subfolder_installation'] && ! $settings ['rewrite_language_alias']) {
54 $uri_parts = explode('/', ltrim($uri, '/'));
55
56 // Remove the first part (subfolder name)
57 $subfolder_prefix = '/' . array_shift($uri_parts);
58
59 // Rebuild URI without subfolder
60 $uri = '/' . implode('/', $uri_parts);
61 }
62
63 $pageLink = rtrim ( get_site_url (), '/' ) . '/' . ltrim ( GPTRANSLATE_CURRENT_LANG, '/' ) . $uri;
64
65 if ( $settings ['serverside_translations_urldecode'] ) {
66 $pageLink = urldecode ( $pageLink );
67 }
68
69 if ($settings ['serverside_translations_ignore_querystring'] == 1) {
70 // Remove query string
71 $pageLink = strtok ( $pageLink, '?' );
72 } elseif (! empty ( $_SERVER ['QUERY_STRING'] )) {
73 // Sanitize and rebuild query string
74 $raw_qs = isset($_SERVER['QUERY_STRING']) ? wp_unslash($_SERVER['QUERY_STRING']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
75
76 parse_str ( $raw_qs, $qs_args );
77
78 $clean_args = [ ];
79 foreach ( $qs_args as $key => $value ) {
80 $clean_key = sanitize_key ( $key );
81 $clean_value = is_array ( $value ) ? array_map ( 'sanitize_text_field', $value ) : sanitize_text_field ( $value );
82 $clean_args [$clean_key] = $clean_value;
83 }
84
85 $clean_qs = http_build_query ( $clean_args, '', '&', PHP_QUERY_RFC3986 );
86
87 if ($clean_qs !== '') {
88 $pageLink = strtok ( $pageLink, '?' ) . '?' . $clean_qs;
89 }
90 }
91
92 // Strip entire query string if any server-injected parameter is detected (e.g. q, fbclid)
93 // Acts as a conditional ignore_querystring=1 triggered by specific parameter names
94 if (!empty($settings['serverside_translations_strip_querystring_params']) && strpos($pageLink, '?') !== false) {
95 $stripParams = array_map('trim', explode(',', strtolower($settings['serverside_translations_strip_querystring_params'])));
96 $pageLinkQs = parse_url($pageLink, PHP_URL_QUERY);
97 if ($pageLinkQs) {
98 parse_str($pageLinkQs, $qsArgs);
99 foreach ($stripParams as $param) {
100 if (isset($qsArgs[$param])) {
101 $pageLink = strtok($pageLink, '?');
102 break;
103 }
104 }
105 }
106 }
107
108 if ( $settings ['serverside_translations_urlencode_space'] ) {
109 $pageLink = str_ireplace ( ' ', '%20', $pageLink );
110 }
111
112 $table = $wpdb->prefix . 'gptranslate';
113
114 if( $settings ['rewrite_language_url'] && $settings ['rewrite_language_alias'] ) {
115 $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
116 "\n WHERE ( pagelink = %s OR pagelink = %s OR translated_alias = %s OR translated_alias = %s) AND languageoriginal = %s AND languagetranslated = %s AND published = 1",
117 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $originalLang, $translatedLang ), ARRAY_A );
118 } else {
119 $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
120 "\n WHERE ( pagelink = %s OR pagelink = %s ) AND languageoriginal = %s AND languagetranslated = %s AND published = 1",
121 rtrim($pageLink, '/'), rtrim($pageLink, '/') . '/', $originalLang, $translatedLang ), ARRAY_A );
122 }
123
124 if (! $row) {
125 return $html;
126 }
127
128 // Load translated aliases to replace on page links
129 $translatedAliasesMap = []; // Map absolute URLs
130 $translatedAliasesRelativeMap = []; // Map relative URL (pathname)
131
132 // Store settings for keep context flatten tags
133 $flattenInnerFormattingTags = isset($settings['flatten_inner_formatting_tags']) ? $settings['flatten_inner_formatting_tags'] : 0;
134 $flattenInnerFormattingTagsToRemove = isset($settings['flatten_inner_formatting_tags_to_remove']) ? explode(',', $settings['flatten_inner_formatting_tags_to_remove']) : explode(',', 'strong,em,u,b,i');
135 $wrapExcludedWords = isset($settings['wrap_excluded_words']) ? $settings['wrap_excluded_words'] : 0;
136
137 if (!empty($settings['rewrite_page_links']) && !empty($settings['rewrite_language_url']) && !empty($settings['rewrite_language_alias'])) {
138 try {
139 $aliasesResult = $wpdb->get_results( $wpdb->prepare(
140 "SELECT pagelink, translated_alias" .
141 "\n FROM {$table}" .
142 "\n WHERE languagetranslated = %s" .
143 "\n AND published = 1",
144 $translatedLang
145 ), ARRAY_A );
146
147 if ($aliasesResult) {
148 $parsedRoot = trailingslashit(get_site_url());
149
150 foreach ($aliasesResult as $rowAliasResult) {
151 // NORMALIZE, WORDPRESS STYLE (KEEP TRAILING SLASH)
152 $pagelink = gpt_trailingslashit_url($rowAliasResult['pagelink']);
153 $translatedAlias = $rowAliasResult['translated_alias'] ?? '';
154
155 if (empty($translatedAlias)) continue;
156
157 $translatedAlias = gpt_trailingslashit_url($translatedAlias);
158
159 // First pass: use decoded pagelink and alias as key (DB stores decoded UTF-8)
160 $translatedAliasesMap[$pagelink] = $translatedAlias;
161
162 // Second pass: use decoded relative path as key
163 $relativePath = str_replace($parsedRoot, '/', $pagelink);
164 $relativePath = parse_url($relativePath, PHP_URL_PATH);
165 if ($relativePath) {
166 $translatedAliasesRelativeMap[gpt_trailingslashit_url($relativePath)] = $translatedAlias;
167 }
168 }
169 }
170 } catch (Exception $e) {
171 // Silently fail
172 }
173 }
174
175 // Process replacement method
176 if ($settings ['serverside_translations_method'] == 'regex') {
177 $translations = json_decode ( $row ['translations'], true ) ?? [ ];
178 $altTranslations = json_decode ( $row ['alt_translations'], true ) ?? [ ];
179 uksort ( $translations, fn ($a, $b) => strlen ( $b ) - strlen ( $a ) );
180 uksort ( $altTranslations, fn ($a, $b) => strlen ( $b ) - strlen ( $a ) );
181
182 // Flatten formatting tags in the body to improve matching if enabled
183 if ($flattenInnerFormattingTags) {
184 $regexTagsToRemove = implode('|', array_map('preg_quote', $flattenInnerFormattingTagsToRemove));
185 function gp_flatten_formatting_tags_regex($html, $regexTagsToRemove) {
186 $pattern = '~<(' . $regexTagsToRemove . ')(\s[^>]*)?>(.*?)</\1>~is';
187
188 // Ripeti finché non ci sono più match (gestisce annidamenti semplici)
189 $prev = null;
190 while ($prev !== $html) {
191 $prev = $html;
192 $html = preg_replace_callback($pattern, function ($m) {
193 // Preserva elementi che contengono form elements o media (select, input, textarea, button, img, ecc.)
194 if (preg_match('/<(select|input|textarea|button|option|optgroup|fieldset|datalist|output|label|form|img|video|audio|canvas|svg|iframe)[\s>\/]/i', $m[3])) {
195 return $m[0]; // restituisci il tag originale intatto
196 }
197 // Preserva elementi vuoti (es. <i> FontAwesome/icon fonts)
198 if (trim(str_replace(['&nbsp;', "\xc2\xa0", "\xa0"], '', strip_tags($m[3]))) === '') {
199 return $m[0];
200 }
201 // tieni SOLO il testo interno (se dentro c’è altro HTML lo butta via)
202 $inner = strip_tags($m[3]);
203 return $inner;
204 }, $html);
205 }
206
207 return $html;
208 }
209 $html = gp_flatten_formatting_tags_regex($html, $regexTagsToRemove);
210 }
211
212 $caseInsensitive = ! empty ( $settings ['serverside_translations_caseinsensitive'] );
213 $matchQuotes = ! empty ( $settings ['serverside_translations_matchquotes'] );
214 $excludedPatterns = [ ];
215
216 $excludedCss = preg_replace ( '/,+/', ',', str_ireplace ( [
217 "\r",
218 "\n",
219 '"'
220 ], [
221 ',',
222 ',',
223 ''
224 ], $settings ['css_selector_serverside_leafnodes_excluded'] ?? '') );
225 $excludedCss = array_filter ( array_map ( 'trim', explode ( ',', $excludedCss ) ) );
226
227 foreach ( $excludedCss as $selector ) {
228 if (preg_match ( '/^([a-z0-9]+)\.(.+)$/i', $selector, $m )) {
229 $excludedPatterns [] = '/<' . preg_quote ( $m [1], '/' ) . '(?=[^>]*\sclass\s*=\s*["\'][^"\']*\b' . preg_quote ( $m [2], '/' ) . '\b)[^>]*>/i';
230 } elseif (preg_match ( '/^\.(.+)$/', $selector, $m )) {
231 $excludedPatterns [] = '/<([a-z0-9]+)(?=[^>]*\sclass\s*=\s*["\'][^"\']*\b' . preg_quote ( $m [1], '/' ) . '\b)[^>]*>/i';
232 }
233 }
234
235 $segments = preg_split ( '/(<[^>]+>)/i', $html, - 1, PREG_SPLIT_DELIM_CAPTURE );
236
237 $skipStack = [ ];
238 foreach ( $segments as $index => $segment ) {
239 if (preg_match ( '/^<\s*(script|style)(\s|>)/i', $segment, $matches )) {
240 $skipStack [] = strtolower ( $matches [1] );
241 } elseif (preg_match ( '/<\/\s*(script|style)[^>]*>/i', $segment, $matches )) { // <--- FIX QUI
242 $tag = strtolower ( $matches [1] );
243 if (! empty ( $skipStack ) && end ( $skipStack ) === $tag) {
244 array_pop ( $skipStack );
245 }
246 } elseif (preg_match ( '/^<\s*([a-zA-Z0-9]+)/', $segment, $tagMatch )) {
247 $tagName = strtolower ( $tagMatch [1] );
248 foreach ( $excludedPatterns as $pattern ) {
249 if (preg_match ( $pattern, $segment )) {
250 $skipStack [] = $tagName;
251 break;
252 }
253 }
254 } elseif (preg_match ( '/^<\/\s*([a-zA-Z0-9]+)/', $segment, $tagMatch )) {
255 $tagName = strtolower ( $tagMatch [1] );
256 if (! empty ( $skipStack ) && end ( $skipStack ) === $tagName) {
257 array_pop ( $skipStack );
258 }
259 } elseif (! preg_match ( '/^<[^>]+>$/', $segment )) {
260 if (empty ( $skipStack )) {
261 if ($matchQuotes) {
262 $segment = str_ireplace ( '"', "'", $segment );
263 }
264 // Normalize once without trimming to preserve spaces between nodes
265 $segment = normalizeTextNoTrim($segment);
266 foreach ( $translations as $originalText => $translatedText ) {
267 $originalText = normalizeText($originalText);
268
269 $prevSegment = $segment;
270 if ($wrapExcludedWords) {
271 // Use word boundaries to avoid sub-replacing inside already translated words
272 $pattern = '/(?<!\w)' . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
273 $segment = preg_replace($pattern, $translatedText, $segment);
274 } else {
275 // Use word boundaries to avoid sub-replacing inside already translated words
276 $pattern = '/(?<!\w)' . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
277 $segment = preg_replace($pattern, $translatedText, $segment);
278 if ($segment !== $prevSegment) {
279 break;
280 }
281 }
282 }
283 }
284 }
285
286 $segments [$index] = $segment;
287 }
288
289 $html = implode ( '', $segments );
290
291 // Replace alt and title in images
292 if (! empty ( $settings ['translate_altimages'] )) {
293 $html = preg_replace_callback ( '/<img[^>]*\b(alt|title)\s*=\s*([\"\'])(.*?)\2[^>]*>/i', function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
294 $attr = $matches [1];
295 $quote = $matches [2];
296 $value = $matches [3];
297 if ($matchQuotes)
298 $value = str_ireplace ( '"', "'", $value );
299 foreach ( $altTranslations as $original => $translated ) {
300 $original = normalizeText($original);
301 $value = normalizeText($value);
302
303 $value = $caseInsensitive ? str_ireplace ( trim ( $original ), $translated, $value ) : str_replace ( trim ( $original ), $translated, $value );
304 }
305 return preg_replace ( '/\b' . $attr . '\s*=\s*["\'].*?["\']/', "$attr=$quote$value$quote", $matches [0] );
306 }, $html );
307
308 // Check if also images 'src' are enabled to be translated
309 if (! empty ( $settings ['translate_srcimages'] )) {
310 $html = preg_replace_callback('/<img\b[^>]*\bsrc\s*=\s*(["\'])(.*?)\1[^>]*>/i', function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
311 $originalTag = $matches[0];
312 $quote = $matches[1];
313 $srcValue = ltrim($matches[2], '/');
314
315 if ($matchQuotes) {
316 $srcValue = str_ireplace('"', "'", $srcValue);
317 }
318
319 $translatedSrcValue = null;
320 foreach ($altTranslations as $originalText => $translatedText) {
321 $originalText = ltrim($originalText, '/');
322 if ($caseInsensitive) {
323 if (strcasecmp(trim($originalText), $srcValue) === 0) {
324 $translatedSrcValue = $translatedText;
325 break;
326 }
327 } else {
328 if (trim($originalText) === $srcValue) {
329 $translatedSrcValue = $translatedText;
330 break;
331 }
332 }
333 }
334
335 // If a translation was found, replace src and srcset
336 if ($translatedSrcValue !== null) {
337 // Replace the src attribute
338 $modifiedTag = preg_replace('/\bsrc\s*=\s*["\'].*?["\']/i', 'src=' . $quote . $translatedSrcValue . $quote, $originalTag);
339
340 // Check if srcset exists and replace it with the translated src value
341 if (preg_match('/\bsrcset\s*=\s*(["\'])(.*?)\1/i', $modifiedTag, $srcsetMatch)) {
342 $srcsetQuote = $srcsetMatch[1];
343 $modifiedTag = preg_replace('/\bsrcset\s*=\s*["\'].*?["\']/i', 'srcset=' . $srcsetQuote . $translatedSrcValue . $srcsetQuote, $modifiedTag);
344 }
345
346 return $modifiedTag;
347 }
348
349 return $originalTag;
350 }, $html);
351 }
352 }
353
354 // Translate iframe locale parameter (e.g. Stripe payment forms)
355 if (! empty ( $settings ['translate_iframe_locale'] )) {
356 $currentLangCode = strtolower(explode('-', $currentLanguage)[0]);
357 $html = preg_replace('/(<iframe[^>]*\bsrc=["\'"])([^"\']*?locale=)[a-z]{2}([^"\']*["\'])/i', '$1${2}' . $currentLangCode . '$3', $html);
358 }
359
360 // Translate <meta name="description" content="...">
361 $html = preg_replace_callback(
362 '~<meta\s+(?:name|property)=["\'](?:description|og:description|twitter:description|dc\.description)["\']\s+content=["\'](.*?)["\'][^>]*>~i',
363 function ($matches) use ($altTranslations, $caseInsensitive, $matchQuotes) {
364 $originalTag = $matches[0];
365 $contentValue = $matches[1];
366
367 // DECODIFICA HTML entities PRIMA del confronto
368 $contentValue = html_entity_decode($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
369
370 if ($matchQuotes) {
371 $contentValue = str_ireplace('"', "'", $contentValue);
372 }
373
374 foreach ($altTranslations as $originalText => $translatedText) {
375 $prev = $contentValue;
376 if ($caseInsensitive) {
377 $contentValue = str_ireplace(trim($originalText), $translatedText, $contentValue);
378 } else {
379 $contentValue = str_replace(trim($originalText), $translatedText, $contentValue);
380 }
381 if ($contentValue !== $prev) break;
382 }
383
384 // RI-ENCODIFICA per l'HTML output (sicurezza)
385 $contentValue = htmlspecialchars($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
386
387 return preg_replace(
388 '/content=["\'].*?["\']/i',
389 'content="' . $contentValue . '"',
390 $originalTag
391 );
392 },
393 $html
394 );
395
396 // Translate <meta property="og:title"> with the same value as <title> tag
397 // Step 1: Extract the translated title content
398 if (preg_match('~<title[^>]*>(.*?)</title>~is', $html, $titleMatch)) {
399 $translatedTitle = trim(strip_tags($titleMatch[1]));
400
401 // Step 2: Replace og:title content directly
402 $html = preg_replace(
403 '~(<meta\s+property=["\']og:title["\']\s+content=["\']).*?(["\'][^>]*>)~i',
404 '$1' . htmlspecialchars($translatedTitle, ENT_QUOTES, 'UTF-8') . '$2',
405 $html
406 );
407
408 // Step 3: Replace twitter:title content with the same translated title
409 $html = preg_replace(
410 '~(<meta\s+name=["\']twitter:title["\']\s+content=["\']).*?(["\'][^>]*>)~i',
411 '$1' . htmlspecialchars($translatedTitle, ENT_QUOTES, 'UTF-8') . '$2',
412 $html
413 );
414
415 // Step 4: Replace Dublin Core (SEOPress) dc.title content with the same translated title
416 $html = preg_replace(
417 '~(<meta\s+name=["\']dc\.title["\']\s+content=["\']).*?(["\'][^>]*>)~i',
418 '$1' . htmlspecialchars($translatedTitle, ENT_QUOTES, 'UTF-8') . '$2',
419 $html
420 );
421 }
422
423 // Add skip marker
424 $html = preg_replace ( '/<body/i', '<body data-gptranslateskip="1" data-gptranslateoriginalalias="' . $row['pagelink'] . '"', $html, 1 );
425
426 // Replace page links in <a href="..."> tags based on translated aliases
427 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
428 $exclusions = $settings['rewrite_page_links_exclusions'] ?? '';
429 $html = preg_replace_callback(
430 '/<a\s+([^>]*\s)?href\s*=\s*(["\'])(.*?)\2([^>]*)>/i',
431 function($matches) use ($translatedAliasesMap, $translatedAliasesRelativeMap, $settings, $exclusions) {
432 $fullTag = $matches[0];
433 $quote = $matches[2];
434 $href = $matches[3];
435
436 // Check if link should be excluded from rewriting
437 if (gptranslate_should_exclude_link($href, $exclusions)) {
438 return $fullTag;
439 }
440
441 // Decode href attribute to match database entries
442 $decodedHref = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
443
444 // Extract hash/fragment if present
445 $hashFragment = '';
446 if (strpos($decodedHref, '#') !== false) {
447 $parts = explode('#', $decodedHref, 2);
448 $decodedHref = $parts[0]; // URL without hash
449 $hashFragment = '#' . $parts[1]; // Save the hash
450 }
451
452 $queryString = '';
453 if ($settings ['ignore_querystring'] == 1 && strpos($decodedHref, '?') !== false) {
454 $parts = explode('?', $decodedHref, 2);
455 $decodedHref = $parts[0];
456 $queryString = '?' . $parts[1];
457 }
458 $originalPathBeforeNormalization = $decodedHref;
459
460 $decodedHref = gpt_trailingslashit_url($decodedHref);
461
462 $translatedAlias = null;
463
464 // First pass, check in absolute URLs
465 if (isset($translatedAliasesMap[$decodedHref])) {
466 $translatedAlias = $translatedAliasesMap[$decodedHref];
467 }
468 // Second pass, check in relative URLs (pathname)
469 elseif (isset($translatedAliasesRelativeMap[$decodedHref])) {
470 $translatedAlias = $translatedAliasesRelativeMap[$decodedHref];
471 }
472
473 // If found a translated alias, replace href and add data-originalhref if not present
474 if ($translatedAlias) {
475 $finalHref = $translatedAlias . $queryString . $hashFragment;
476
477 $originalHrefFull = $originalPathBeforeNormalization . $queryString . $hashFragment;
478
479 // Check if data-originalhref already exists in the tag
480 if (strpos($fullTag, 'data-originalhref') === false) {
481 $dataAttr = ' data-originalhref=' . $quote . htmlspecialchars($originalHrefFull, ENT_QUOTES, 'UTF-8') . $quote;
482 $newTag = str_ireplace('<a ', '<a' . $dataAttr . ' ', $fullTag);
483 $newTag = str_ireplace('href=' . $quote . $href . $quote, 'href=' . $quote . htmlspecialchars($finalHref, ENT_QUOTES, 'UTF-8') . $quote, $newTag);
484 return $newTag;
485 } else {
486 return str_ireplace('href=' . $quote . $href . $quote, 'href=' . $quote . htmlspecialchars($finalHref, ENT_QUOTES, 'UTF-8') . $quote, $fullTag);
487 }
488 }
489
490 return $fullTag;
491 },
492 $html
493 );
494
495 // Replace form action URLs: add language prefix and apply translated aliases
496 if (!empty($settings['rewrite_form_actions'])) {
497 $siteUrl = trailingslashit(get_site_url());
498 $siteHost = parse_url($siteUrl, PHP_URL_HOST);
499 $knownLangs = (isset($settings['languages']) && is_array($settings['languages'])) ? array_map('strtolower', $settings['languages']) : [$originalLang, $translatedLang];
500 $html = preg_replace_callback(
501 '/<form\s+([^>]*\s)?action\s*=\s*(["\'])(.*?)\2([^>]*)>/i',
502 function($matches) use ($translatedAliasesMap, $translatedAliasesRelativeMap, $settings, $translatedLang, $siteUrl, $siteHost, $knownLangs) {
503 $fullTag = $matches[0];
504 $quote = $matches[2];
505 $action = $matches[3];
506
507 $decodedAction = rawurldecode(html_entity_decode($action, ENT_QUOTES, 'UTF-8'));
508
509 // Skip anchors, mailto, tel, javascript, .php endpoints
510 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $decodedAction)) return $fullTag;
511 if (preg_match('/\.php($|\?|\#)/i', $decodedAction)) return $fullTag;
512
513 // Skip external URLs
514 if (preg_match('/^https?:\/\//i', $decodedAction)) {
515 $actionHost = parse_url($decodedAction, PHP_URL_HOST);
516 if ($actionHost !== $siteHost) return $fullTag;
517 }
518
519 // Extract query string
520 $queryString = '';
521 if ($settings['ignore_querystring'] == 1 && strpos($decodedAction, '?') !== false) {
522 $parts = explode('?', $decodedAction, 2);
523 $decodedAction = $parts[0];
524 $queryString = '?' . $parts[1];
525 }
526 $originalActionFull = $decodedAction . $queryString;
527
528 // Extract relative path
529 $actionPath = $decodedAction;
530 $isAbsolute = false;
531 if (preg_match('/^https?:\/\//i', $actionPath)) {
532 $isAbsolute = true;
533 $actionPath = parse_url($actionPath, PHP_URL_PATH) ?: '/';
534 }
535
536 // Split path and determine language index
537 $pathParts = explode('/', $actionPath);
538 $langIndex = 1;
539 if (isset($pathParts[$langIndex]) && $pathParts[$langIndex] === 'index.php') {
540 $langIndex = 2;
541 }
542 if (!empty($settings['subfolder_installation'])) {
543 $langIndex = 2;
544 }
545
546 // Check if already has a language prefix, replace or insert
547 if (isset($pathParts[$langIndex]) && in_array(strtolower($pathParts[$langIndex]), $knownLangs)) {
548 $pathParts[$langIndex] = $translatedLang;
549 } else {
550 array_splice($pathParts, $langIndex, 0, [$translatedLang]);
551 }
552
553 // Rebuild path
554 $rebuiltPath = '/' . implode('/', array_filter($pathParts, function($p) { return $p !== ''; }));
555 $rebuiltPath = gpt_trailingslashit_url($rebuiltPath);
556
557 // Rebuild full URL if original was absolute
558 $computedAction = $isAbsolute ? (rtrim($siteUrl, '/') . $rebuiltPath) : $rebuiltPath;
559 $computedAction = gpt_trailingslashit_url($computedAction);
560
561 // Try alias match
562 $translatedAlias = null;
563 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
564 if (isset($translatedAliasesMap[$computedAction])) {
565 $translatedAlias = $translatedAliasesMap[$computedAction];
566 } elseif (isset($translatedAliasesRelativeMap[$rebuiltPath])) {
567 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPath];
568 } elseif (!$isAbsolute && !empty($settings['subfolder_installation'])) {
569 // Fallback: form action is relative and includes subfolder path
570 // Map keys strip the subfolder, so retry after stripping it
571 $siteBasePath = rtrim(parse_url(rtrim($siteUrl, '/'), PHP_URL_PATH) ?: '', '/');
572 if ($siteBasePath !== '' && strpos($rebuiltPath, $siteBasePath . '/') === 0) {
573 $rebuiltPathNoSubfolder = substr($rebuiltPath, strlen($siteBasePath));
574 if (isset($translatedAliasesRelativeMap[$rebuiltPathNoSubfolder])) {
575 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPathNoSubfolder];
576 }
577 }
578 }
579 }
580
581 $finalAction = $translatedAlias ? ($translatedAlias . $queryString) : ($computedAction . $queryString);
582
583 if (strpos($fullTag, 'data-originalaction') === false) {
584 $dataAttr = ' data-originalaction=' . $quote . htmlspecialchars($originalActionFull, ENT_QUOTES, 'UTF-8') . $quote;
585 $newTag = str_ireplace('<form ', '<form' . $dataAttr . ' ', $fullTag);
586 $newTag = str_ireplace('action=' . $quote . $action . $quote, 'action=' . $quote . htmlspecialchars($finalAction, ENT_QUOTES, 'UTF-8') . $quote, $newTag);
587 return $newTag;
588 } else {
589 return str_ireplace('action=' . $quote . $action . $quote, 'action=' . $quote . htmlspecialchars($finalAction, ENT_QUOTES, 'UTF-8') . $quote, $fullTag);
590 }
591 },
592 $html
593 );
594 } // end rewrite_form_actions
595 }
596 } elseif ($settings ['serverside_translations_method'] == 'domdocument') {
597 // Solution 2: classic DOMDocument approach, effective but could cause closing tags and encoding issues
598 try {
599 $translationsArray = json_decode ( $row['translations'], true ) ?? [ ];
600 $altTranslationsArray = json_decode ( $row['alt_translations'] ?? '', true ) ?: [ ];
601
602 // Sort the translation keys in descending order by length.
603 uksort ( $translationsArray, function ( $a, $b ) {
604 return strlen ( $b ) - strlen ( $a );
605 } );
606
607 // Sort the alt translation keys in descending order by length.
608 uksort ( $altTranslationsArray, function ( $a, $b ) {
609 return strlen ( $b ) - strlen ( $a );
610 } );
611
612 // Flatten formatting tags in the body to improve matching if enabled
613 if($flattenInnerFormattingTags) {
614 function gp_flatten_inner_formatting_tags($html, $tagsToRemove) {
615 $dom = new DOMDocument();
616 libxml_use_internal_errors(true);
617
618 // Usa lo stesso metodo del codice principale con l'hack XML encoding
619 $dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
620
621 libxml_clear_errors();
622
623 $xpath = new DOMXPath($dom);
624
625 // Seleziona tutti i tag da rimuovere, ovunque
626 $query = '//' . implode(' | //', array_map(function($t) { return strtolower($t); }, $tagsToRemove));
627
628 $nodes = $xpath->query($query);
629
630 // Importante: sostituire dal "basso verso l'alto" per evitare problemi mentre modifichi il DOM
631 // Convertiamo NodeList in array e invertiamo
632 $toReplace = [];
633 foreach ($nodes as $n)
634 $toReplace[] = $n;
635 $toReplace = array_reverse($toReplace);
636
637 foreach ($toReplace as $node) {
638 // Preserva elementi vuoti (es. <i> FontAwesome/icon fonts)
639 if (trim(str_replace(['&nbsp;', "\xc2\xa0", "\xa0"], '', $node->textContent)) === '') {
640 continue;
641 }
642 // Preserva elementi che contengono form elements (select, input, textarea, button, ecc.)
643 $interactiveTags = ['select', 'input', 'textarea', 'button', 'option', 'optgroup', 'fieldset', 'datalist', 'output', 'label', 'form', 'img', 'video', 'audio', 'canvas', 'svg', 'iframe'];
644 $hasInteractive = false;
645 foreach ($interactiveTags as $iTag) {
646 if ($node->getElementsByTagName($iTag)->length > 0) {
647 $hasInteractive = true;
648 break;
649 }
650 }
651 if ($hasInteractive) {
652 continue;
653 }
654
655 $text = $node->textContent; // prende il testo "visibile" (quello che vuoi)
656 $textNode = $dom->createTextNode($text);
657
658 if ($node->parentNode) {
659 $node->parentNode->replaceChild($textNode, $node);
660 }
661 }
662
663 // Salva l'HTML modificato
664 $out = $dom->saveHTML();
665
666 return $out;
667 }
668 $html = gp_flatten_inner_formatting_tags($html, $flattenInnerFormattingTagsToRemove);
669 }
670
671 // Initialize DOMDocument without converting the body content via mb_convert_encoding.
672 $doc = new DOMDocument ();
673 libxml_use_internal_errors ( true );
674 // Use an XML encoding hack to inform DOMDocument about UTF-8 while preserving original characters.
675 $doc->loadHTML ( '<?xml encoding="UTF-8">' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
676 libxml_clear_errors ();
677
678 // Use XPath to locate text nodes, skipping those in <script> or <style> tags.
679 $xpath = new DOMXPath ( $doc );
680 $cssSelectorLeafnodesExcluded = str_ireplace ( '"', '', trim ( trim ( preg_replace ( '/,+/', ',', str_ireplace ( [ "\r", "\n" ], ",", $settings['css_selector_serverside_leafnodes_excluded'] ?? '' ) ) ), ',' ) );
681 $excludedNodes = [ ];
682
683 if (! empty ( $cssSelectorLeafnodesExcluded )) {
684 $selectors = explode ( ',', $cssSelectorLeafnodesExcluded );
685 $xpathQueries = [ ];
686
687 foreach ( $selectors as $selector ) {
688 $selector = trim ( $selector );
689 if (empty ( $selector ))
690 continue;
691
692 // Convert CSS selector to XPath
693 $selector = preg_replace ( '/\s+/', ' ', $selector ); // Normalize spaces
694 $selectorParts = explode ( ' ', $selector );
695
696 $xpathQuery = '';
697 foreach ( $selectorParts as $part ) {
698 if (preg_match ( '/^([a-zA-Z0-9_-]+)?(\.[a-zA-Z0-9_-]+)?(#[a-zA-Z0-9_-]+)?$/', $part, $matches )) {
699 $tag = ! empty ( $matches[1] ) ? $matches[1] : '*'; // If no tag, use '*'
700 $class = ! empty ( $matches[2] ) ? substr ( $matches[2], 1 ) : ''; // Remove leading '.'
701 $id = ! empty ( $matches[3] ) ? substr ( $matches[3], 1 ) : ''; // Remove leading '#'
702
703 $conditions = [ ];
704 if ($class) {
705 $conditions[] = "contains(concat(' ', normalize-space(@class), ' '), ' $class ')";
706 }
707 if ($id) {
708 $conditions[] = "@id='$id'";
709 }
710
711 $xpathQuery .= "//{$tag}" . (! empty ( $conditions ) ? "[" . implode ( " and ", $conditions ) . "]" : "");
712 }
713 }
714
715 if (! empty ( $xpathQuery )) {
716 $xpathQueries[] = $xpathQuery;
717 }
718 }
719
720 // Execute all XPath queries and collect excluded nodes
721 foreach ( $xpathQueries as $query ) {
722 foreach ( $xpath->query ( $query ) as $excludedNode ) {
723 $excludedNodes[] = $excludedNode;
724 }
725 }
726 }
727
728 $textNodes = $xpath->query ( '//text()[not(ancestor::script) and not(ancestor::style) and normalize-space()]' );
729 $caseInsensitive = ! empty ( $settings['serverside_translations_caseinsensitive'] );
730 $matchQuotes = ! empty ( $settings['serverside_translations_matchquotes'] );
731
732 // Process each text node with translation replacements.
733 foreach ( $textNodes as $textNode ) {
734 if ($cssSelectorLeafnodesExcluded) {
735 $skipNode = false;
736 foreach ( $excludedNodes as $excludedNode ) {
737 if ($excludedNode->isSameNode ( $textNode->parentNode )) {
738 $skipNode = true;
739 break;
740 }
741 }
742 if ($skipNode)
743 continue;
744 }
745
746 $originalTextContent = $textNode->nodeValue;
747 $textContent = $textNode->nodeValue;
748 if ($matchQuotes) {
749 $textContent = str_ireplace ( '"', "'", $textContent );
750 }
751 // Normalize once without trimming to preserve spaces between nodes
752 $textContent = normalizeTextNoTrim($textContent);
753 foreach ( $translationsArray as $originalText => $translatedText ) {
754 $originalText = normalizeText($originalText);
755
756 $prevTextContent = $textContent; // Save the current state
757 if ($wrapExcludedWords) {
758 // Use word boundaries to avoid sub-replacing inside already translated words
759 $pattern = '/(?<!\w)' . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
760 $textContent = preg_replace($pattern, $translatedText, $textContent);
761 } else {
762 // Use word boundaries to avoid sub-replacing inside already translated words
763 $pattern = '/(?<!\w)' . preg_quote(trim($originalText), '/') . '(?!\w)/' . ($caseInsensitive ? 'ui' : 'u');
764 $textContent = preg_replace($pattern, $translatedText, $textContent);
765 if ($textContent !== $prevTextContent) {
766 break;
767 }
768 }
769 }
770 $textNode->nodeValue = $textContent;
771 }
772
773 // Replace page links in <a href="..."> tags based on translated aliases
774 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
775 $exclusions = $settings['rewrite_page_links_exclusions'] ?? '';
776 $linkNodes = $xpath->query('//a[@href]');
777 foreach ($linkNodes as $linkNode) {
778 $href = $linkNode->getAttribute('href');
779
780 // Check if link should be excluded from rewriting
781 if (gptranslate_should_exclude_link($href, $exclusions)) {
782 continue;
783 }
784
785 // Decode href attribute (WordPress-style, KEEP trailing slash)
786 $decodedHref = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
787
788 // Extract hash/fragment if present
789 $hashFragment = '';
790 if (strpos($decodedHref, '#') !== false) {
791 $parts = explode('#', $decodedHref, 2);
792 $decodedHref = $parts[0]; // URL without hash
793 $hashFragment = '#' . $parts[1]; // Save the hash
794 }
795
796 $queryString = '';
797 if ($settings['ignore_querystring'] == 1 && strpos($decodedHref, '?') !== false) {
798 $parts = explode('?', $decodedHref, 2);
799 $decodedHref = $parts[0];
800 $queryString = '?' . $parts[1];
801 }
802 $originalPathBeforeNormalization = $decodedHref;
803
804 $decodedHref = gpt_trailingslashit_url($decodedHref);
805
806 $translatedAlias = null;
807
808 // Cerca prima negli URL assoluti
809 if (isset($translatedAliasesMap[$decodedHref])) {
810 $translatedAlias = $translatedAliasesMap[$decodedHref];
811 }
812 // Poi cerca negli URL relativi
813 elseif (isset($translatedAliasesRelativeMap[$decodedHref])) {
814 $translatedAlias = $translatedAliasesRelativeMap[$decodedHref];
815 }
816
817 // Se trovato un alias tradotto, sostituisci
818 if ($translatedAlias) {
819 // Aggiungi data-originalhref se non presente
820 if (!$linkNode->hasAttribute('data-originalhref')) {
821 $linkNode->setAttribute('data-originalhref', $originalPathBeforeNormalization . $queryString . $hashFragment);
822 }
823 $linkNode->setAttribute('href', $translatedAlias . $queryString . $hashFragment);
824 }
825 }
826 }
827
828 // Replace form action URLs: add language prefix and apply translated aliases
829 if (!empty($settings['rewrite_form_actions'])) {
830 $siteUrl = trailingslashit(get_site_url());
831 $siteHost = parse_url($siteUrl, PHP_URL_HOST);
832 $knownLangs = (isset($settings['languages']) && is_array($settings['languages'])) ? array_map('strtolower', $settings['languages']) : [$originalLang, $translatedLang];
833 $formNodes = $xpath->query('//form[@action]');
834 foreach ($formNodes as $formNode) {
835 $action = $formNode->getAttribute('action');
836 $decodedAction = rawurldecode(html_entity_decode($action, ENT_QUOTES, 'UTF-8'));
837
838 // Skip non-rewritable actions
839 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $decodedAction)) continue;
840 if (preg_match('/\.php($|\?|\#)/i', $decodedAction)) continue;
841 if (preg_match('/^https?:\/\//i', $decodedAction) && parse_url($decodedAction, PHP_URL_HOST) !== $siteHost) continue;
842
843 $queryString = '';
844 if ($settings['ignore_querystring'] == 1 && strpos($decodedAction, '?') !== false) {
845 $parts = explode('?', $decodedAction, 2);
846 $decodedAction = $parts[0];
847 $queryString = '?' . $parts[1];
848 }
849 $originalActionFull = $decodedAction . $queryString;
850
851 // Extract relative path
852 $actionPath = $decodedAction;
853 $isAbsolute = false;
854 if (preg_match('/^https?:\/\//i', $actionPath)) {
855 $isAbsolute = true;
856 $actionPath = parse_url($actionPath, PHP_URL_PATH) ?: '/';
857 }
858
859 // Add/replace language prefix
860 $pathParts = explode('/', $actionPath);
861 $langIndex = 1;
862 if (isset($pathParts[$langIndex]) && $pathParts[$langIndex] === 'index.php') $langIndex = 2;
863 if (!empty($settings['subfolder_installation'])) $langIndex = 2;
864
865 if (isset($pathParts[$langIndex]) && in_array(strtolower($pathParts[$langIndex]), $knownLangs)) {
866 $pathParts[$langIndex] = $translatedLang;
867 } else {
868 array_splice($pathParts, $langIndex, 0, [$translatedLang]);
869 }
870
871 $rebuiltPath = '/' . implode('/', array_filter($pathParts, function($p) { return $p !== ''; }));
872 $rebuiltPath = gpt_trailingslashit_url($rebuiltPath);
873 $computedAction = $isAbsolute ? (rtrim($siteUrl, '/') . $rebuiltPath) : $rebuiltPath;
874 $computedAction = gpt_trailingslashit_url($computedAction);
875
876 // Try alias match
877 $translatedAlias = null;
878 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
879 if (isset($translatedAliasesMap[$computedAction])) {
880 $translatedAlias = $translatedAliasesMap[$computedAction];
881 } elseif (isset($translatedAliasesRelativeMap[$rebuiltPath])) {
882 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPath];
883 } elseif (!$isAbsolute && !empty($settings['subfolder_installation'])) {
884 // Fallback: form action is relative and includes subfolder path
885 // Map keys strip the subfolder, so retry after stripping it
886 $siteBasePath = rtrim(parse_url(rtrim($siteUrl, '/'), PHP_URL_PATH) ?: '', '/');
887 if ($siteBasePath !== '' && strpos($rebuiltPath, $siteBasePath . '/') === 0) {
888 $rebuiltPathNoSubfolder = substr($rebuiltPath, strlen($siteBasePath));
889 if (isset($translatedAliasesRelativeMap[$rebuiltPathNoSubfolder])) {
890 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPathNoSubfolder];
891 }
892 }
893 }
894 }
895
896 $finalAction = $translatedAlias ? ($translatedAlias . $queryString) : ($computedAction . $queryString);
897
898 if (!$formNode->hasAttribute('data-originalaction')) {
899 $formNode->setAttribute('data-originalaction', $originalActionFull);
900 }
901 $formNode->setAttribute('action', $finalAction);
902 }
903 }
904
905 // Check if also images 'alt' are enabled to be translated
906 if (! empty ( $settings['translate_altimages'] )) {
907 $imgNodesAlt = $xpath->query ( '//img[@alt]' );
908 foreach ( $imgNodesAlt as $imgNode ) {
909 $altText = $imgNode->getAttribute ( 'alt' );
910 if ($matchQuotes) {
911 $altText = str_ireplace ( '"', "'", $altText );
912 }
913 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
914 $originalText = normalizeText($originalText);
915 $altText = normalizeText($altText);
916
917 $prevAltText = $altText;
918 if ($caseInsensitive) {
919 $altText = str_ireplace ( trim ( $originalText ), $translatedText, $altText );
920 } else {
921 $altText = str_replace ( trim ( $originalText ), $translatedText, $altText );
922 }
923 if ($altText !== $prevAltText) {
924 break;
925 }
926 }
927 $imgNode->setAttribute ( 'alt', $altText );
928 }
929
930 $imgNodesTitle = $xpath->query ( '//img[@title]' );
931 foreach ( $imgNodesTitle as $imgNode ) {
932 $titleText = $imgNode->getAttribute ( 'title' );
933 if ($matchQuotes) {
934 $titleText = str_ireplace ( '"', "'", $titleText );
935 }
936 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
937 $originalText = normalizeText($originalText);
938 $titleText = normalizeText($titleText);
939
940 $prevTitleText = $titleText;
941 if ($caseInsensitive) {
942 $titleText = str_ireplace ( trim ( $originalText ), $translatedText, $titleText );
943 } else {
944 $titleText = str_replace ( trim ( $originalText ), $translatedText, $titleText );
945 }
946 if ($titleText !== $prevTitleText) {
947 break;
948 }
949 }
950 $imgNode->setAttribute ( 'title', $titleText );
951 }
952
953 // Check if also images 'src' are enabled to be translated
954 if (! empty ( $settings ['translate_srcimages'] )) {
955 $imgNodesSrc = $xpath->query ( '//img[@src]' );
956 foreach ( $imgNodesSrc as $imgNode ) {
957 $srcValue = ltrim ( $imgNode->getAttribute ( 'src' ), '/' );
958
959 if ($matchQuotes) {
960 $srcValue = str_ireplace ( '"', "'", $srcValue );
961 }
962
963 $translatedSrcValue = null;
964 foreach ( $altTranslationsArray as $originalText => $translatedText ) {
965 $originalText = normalizeText($originalText);
966 $srcValue = normalizeText($srcValue);
967
968 $originalText = ltrim ( $originalText, '/' );
969 if ($caseInsensitive) {
970 if (strcasecmp ( trim ( $originalText ), $srcValue ) === 0) {
971 $translatedSrcValue = $translatedText;
972 break;
973 }
974 } else {
975 if (trim ( $originalText ) === $srcValue) {
976 $translatedSrcValue = $translatedText;
977 break;
978 }
979 }
980 }
981
982 // If a translation was found, replace src and srcset
983 if ($translatedSrcValue !== null) {
984 // Replace the src attribute
985 $imgNode->setAttribute ( 'src', $translatedSrcValue );
986
987 // Check if srcset exists and replace it with the translated src value
988 if ($imgNode->hasAttribute ( 'srcset' )) {
989 $imgNode->setAttribute ( 'srcset', $translatedSrcValue );
990 }
991 }
992 }
993 }
994 }
995
996 // Translate iframe locale parameter (e.g. Stripe payment forms)
997 if (! empty ( $settings ['translate_iframe_locale'] )) {
998 $currentLangCode = strtolower(explode('-', $currentLanguage)[0]);
999 $iframeNodes = $xpath->query ( '//iframe[@src]' );
1000 foreach ( $iframeNodes as $iframeNode ) {
1001 $src = $iframeNode->getAttribute ( 'src' );
1002 if (strpos ( $src, 'locale=' ) !== false) {
1003 $translatedSrc = preg_replace ( '/locale=[a-z]{2}/i', 'locale=' . $currentLangCode, $src );
1004 $iframeNode->setAttribute ( 'src', $translatedSrc );
1005 }
1006 }
1007 }
1008
1009 // Translate meta descriptions (standard, Open Graph, Twitter, Dublin Core)
1010 $metaDescriptions = $xpath->query(
1011 '//meta[
1012 (@name="description")
1013 or (@name="twitter:description")
1014 or (@property="og:description")
1015 or (@name="dc.description")
1016 ]'
1017 );
1018 foreach ($metaDescriptions as $meta) {
1019 $contentValue = $meta->getAttribute('content');
1020
1021 if ($matchQuotes) {
1022 $contentValue = str_ireplace('"', "'", $contentValue);
1023 }
1024
1025 foreach ($altTranslationsArray as $originalText => $translatedText) {
1026 $prev = $contentValue;
1027 if ($caseInsensitive) {
1028 $contentValue = str_ireplace(trim($originalText), $translatedText, $contentValue);
1029 } else {
1030 $contentValue = str_replace(trim($originalText), $translatedText, $contentValue);
1031 }
1032 if ($contentValue !== $prev) break;
1033 }
1034
1035 $meta->setAttribute('content', $contentValue);
1036 }
1037
1038 $ogTitleNodes = $xpath->query('//meta[@property="og:title"]');
1039 if ($ogTitleNodes->length > 0) {
1040 // Find the <title> tag to get the translated title
1041 $titleNodes = $xpath->query('//title');
1042
1043 if ($titleNodes->length > 0) {
1044 $titleNode = $titleNodes->item(0);
1045 $translatedTitle = $titleNode->textContent;
1046
1047 // Set og:title with the same value as <title>
1048 foreach ($ogTitleNodes as $ogTitleNode) {
1049 $ogTitleNode->setAttribute('content', trim($translatedTitle));
1050 }
1051
1052 // Set twitter:title with the same value as <title>
1053 $twitterTitleNodes = $xpath->query('//meta[@name="twitter:title"]');
1054 foreach ($twitterTitleNodes as $twitterTitleNode) {
1055 $twitterTitleNode->setAttribute('content', trim($translatedTitle));
1056 }
1057
1058 // Set Dublin Core (SEOPress) dc:title with the same value as <title>
1059 $dcTitleNodes = $xpath->query('//meta[@name="dc.title"]');
1060 foreach ($dcTitleNodes as $dcTitleNode) {
1061 $dcTitleNode->setAttribute('content', trim($translatedTitle));
1062 }
1063 }
1064 }
1065
1066 // Optionally, add an attribute to the <body> tag to signal that translations were applied.
1067 $bodyTag = $doc->getElementsByTagName ( 'body' )->item ( 0 );
1068 if ($bodyTag) {
1069 $bodyTag->setAttribute ( 'data-gptranslateskip', '1' );
1070 $bodyTag->setAttribute ( 'data-gptranslateoriginalalias', htmlspecialchars($row['pagelink'], ENT_QUOTES, 'UTF-8') );
1071 }
1072
1073 // Save the modified HTML.
1074 $html = $doc->saveHTML ();
1075
1076 // Decode HTML entities back into UTF-8 characters.
1077 $html = html_entity_decode ( $html, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
1078
1079 } catch ( Exception $e ) {
1080 // Handle exception if needed
1081 }
1082 } elseif ($settings['serverside_translations_method'] == 'simplehtmldom') {
1083 require_once plugin_dir_path(__FILE__) . 'simplehtmldom.php';
1084
1085 // Helper: checks if $child is inside (or is) $parent.
1086 function nodeIsInsideExcluded($child, $excludedNodes) {
1087 while ($child !== null) {
1088 foreach ($excludedNodes as $ex) {
1089 if ($child === $ex) {
1090 return true;
1091 }
1092 }
1093 $child = $child->parent;
1094 }
1095 return false;
1096 }
1097
1098 // Helper: checks if $node is inside (or is) any of the specified tags.
1099 function nodeIsInsideTags($node, $tagNames) {
1100 $tagNames = array_map('strtolower', $tagNames);
1101 while ($node !== null) {
1102 $tag = strtolower($node->tag ?? '');
1103 if (in_array($tag, $tagNames, true)) {
1104 return true;
1105 }
1106 $node = $node->parent;
1107 }
1108 return false;
1109 }
1110
1111 // Recursive function to process all text nodes.
1112 function processTextNodes($node, $excludedNodes, $translationsArray, $caseInsensitive, $matchQuotes) {
1113 if ($node->tag === 'text') {
1114 if (! nodeIsInsideExcluded($node, $excludedNodes) && ! nodeIsInsideTags($node, ['script', 'style', 'noscript']) && trim($node->innertext) && $node->innertext != "\t") {
1115 $text = $node->innertext;
1116 if ($matchQuotes) {
1117 $text = str_ireplace('"', "'", $text);
1118 }
1119
1120 // Normalize once to avoid stripping spaces added by replacements
1121 $text = normalizeTextNoTrim($text);
1122
1123 // Apply translations without overriding longer translations
1124 $processedParts = [];
1125 foreach ($translationsArray as $originalText => $translatedText) {
1126 $originalText = normalizeText($originalText);
1127
1128 if ($caseInsensitive) {
1129 $text = preg_replace_callback(
1130 '/(?<!\w)' . preg_quote(trim($originalText), '/') . '(?!\w)/ui',
1131 function ($matches) use ($translatedText, &$processedParts) {
1132 if (in_array($matches[0], $processedParts, true)) {
1133 return $matches[0];
1134 }
1135 $processedParts[] = $translatedText;
1136 return $translatedText;
1137 },
1138 $text
1139 );
1140 } else {
1141 $text = preg_replace_callback(
1142 '/(?<!\w)' . preg_quote(trim($originalText), '/') . '(?!\w)/',
1143 function ($matches) use ($translatedText, &$processedParts) {
1144 if (in_array($matches[0], $processedParts, true)) {
1145 return $matches[0];
1146 }
1147 $processedParts[] = $translatedText;
1148 return $translatedText;
1149 },
1150 $text
1151 );
1152 }
1153 }
1154 $node->innertext = $text;
1155 }
1156 } else {
1157 $tagLower = strtolower($node->tag ?? '');
1158 if (in_array($tagLower, ['script', 'style'])) {
1159 return;
1160 }
1161 if (isset($node->nodes) && is_array($node->nodes)) {
1162 foreach ($node->nodes as $child) {
1163 processTextNodes($child, $excludedNodes, $translationsArray, $caseInsensitive, $matchQuotes);
1164 }
1165 }
1166 }
1167 }
1168
1169 // Main processing code.
1170 try {
1171 $translationsArray = json_decode($row['translations'], true) ?? [];
1172 $altTranslationsArray = json_decode($row['alt_translations'] ?? '', true) ?: [];
1173
1174 // Sort translations by descending key length
1175 uksort($translationsArray, function ($a, $b) {
1176 return strlen($b) - strlen($a);
1177 });
1178
1179 // Sort alt translations by descending key length
1180 uksort($altTranslationsArray, function ($a, $b) {
1181 return strlen($b) - strlen($a);
1182 });
1183
1184 // Flatten formatting tags regex
1185 if($flattenInnerFormattingTags) {
1186 // Flatten formatting tags in the body to improve matching if enabled
1187 function gp_flatten_inner_formatting_tags_simplehtmldom($htmlString, $tagsToRemove ) {
1188 // Complete body parsing
1189 $doc = gptranslate_simplehtmldom_str_get_html ( $htmlString );
1190 if (! $doc)
1191 return $htmlString;
1192
1193 $selector = implode ( ',', $tagsToRemove );
1194
1195 $found = $doc->find($selector);
1196 if (!empty($found)) {
1197 for ($i = count($found) - 1; $i >= 0; $i--) {
1198 $n = $found[$i];
1199 // Preserva elementi vuoti (es. <i> FontAwesome/icon fonts)
1200 if (trim(str_replace(['&nbsp;', "\xc2\xa0", "\xa0"], '', $n->plaintext)) === '') {
1201 continue;
1202 }
1203 // Preserva elementi che contengono form elements (select, input, textarea, button, ecc.)
1204 $interactiveTags = ['select', 'input', 'textarea', 'button', 'option', 'optgroup', 'fieldset', 'datalist', 'output', 'label', 'form', 'img', 'video', 'audio', 'canvas', 'svg', 'iframe'];
1205 $hasInteractive = false;
1206 foreach ($interactiveTags as $iTag) {
1207 if (!empty($n->find($iTag))) {
1208 $hasInteractive = true;
1209 break;
1210 }
1211 }
1212 if ($hasInteractive) {
1213 continue;
1214 }
1215 $n->outertext = $n->plaintext;
1216 }
1217 }
1218
1219 $out = $doc->save();
1220
1221 if (method_exists($doc, 'clear')) $doc->clear();
1222 unset($doc);
1223
1224 return $out;
1225 }
1226 $html = gp_flatten_inner_formatting_tags_simplehtmldom($html, $flattenInnerFormattingTagsToRemove);
1227 }
1228
1229 $htmlObj = gptranslate_simplehtmldom_str_get_html($html);
1230
1231 $cssSelectorLeafnodesExcluded = str_ireplace(
1232 '"',
1233 '',
1234 trim(trim(preg_replace('/,+/', ',', str_ireplace(["\r", "\n"], ",", $settings['css_selector_serverside_leafnodes_excluded'] ?? ''))), ',')
1235 );
1236
1237 $excludedNodes = [];
1238 if (! empty($cssSelectorLeafnodesExcluded)) {
1239 $selectors = explode(',', $cssSelectorLeafnodesExcluded);
1240 foreach ($selectors as $selector) {
1241 $selector = trim($selector);
1242 if (! empty($selector)) {
1243 $foundNodes = $htmlObj->find($selector);
1244 foreach ($foundNodes as $node) {
1245 $excludedNodes[] = $node;
1246 }
1247 }
1248 }
1249 }
1250
1251 $caseInsensitive = ! empty($settings['serverside_translations_caseinsensitive']);
1252 $matchQuotes = ! empty($settings['serverside_translations_matchquotes']);
1253
1254 processTextNodes($htmlObj, $excludedNodes, $translationsArray, $caseInsensitive, $matchQuotes);
1255
1256 // Replace page links in <a href="..."> tags based on translated aliases
1257 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
1258 $exclusions = $settings['rewrite_page_links_exclusions'] ?? '';
1259 foreach ($htmlObj->find('a[href]') as $linkNode) {
1260 $href = $linkNode->href;
1261
1262 // Check if link should be excluded from rewriting
1263 if (gptranslate_should_exclude_link($href, $exclusions)) {
1264 continue;
1265 }
1266
1267 // Decode href attribute (WordPress-style, KEEP trailing slash)
1268 $decodedHref = rawurldecode(html_entity_decode($href, ENT_QUOTES, 'UTF-8'));
1269
1270 // Extract hash/fragment if present
1271 $hashFragment = '';
1272 if (strpos($decodedHref, '#') !== false) {
1273 $parts = explode('#', $decodedHref, 2);
1274 $decodedHref = $parts[0]; // URL without hash
1275 $hashFragment = '#' . $parts[1]; // Save the hash
1276 }
1277
1278 // Extract query string if ignore_querystring is enabled
1279 $queryString = '';
1280 if ($settings['ignore_querystring'] == 1 && strpos($decodedHref, '?') !== false) {
1281 $parts = explode('?', $decodedHref, 2);
1282 $decodedHref = $parts[0];
1283 $queryString = '?' . $parts[1];
1284 }
1285 $originalPathBeforeNormalization = $decodedHref;
1286
1287 $decodedHref = gpt_trailingslashit_url($decodedHref);
1288
1289 $translatedAlias = null;
1290
1291 // Cerca prima negli URL assoluti
1292 if (isset($translatedAliasesMap[$decodedHref])) {
1293 $translatedAlias = $translatedAliasesMap[$decodedHref];
1294 }
1295 // Poi cerca negli URL relativi
1296 elseif (isset($translatedAliasesRelativeMap[$decodedHref])) {
1297 $translatedAlias = $translatedAliasesRelativeMap[$decodedHref];
1298 }
1299
1300 // Se trovato un alias tradotto, sostituisci
1301 if ($translatedAlias) {
1302 // Aggiungi data-originalhref se non presente
1303 if (!isset($linkNode->{'data-originalhref'})) {
1304 $linkNode->{'data-originalhref'} = $originalPathBeforeNormalization . $queryString . $hashFragment;
1305 }
1306 $linkNode->href = $translatedAlias . $queryString . $hashFragment;
1307 }
1308 }
1309 }
1310
1311 // Replace form action URLs: add language prefix and apply translated aliases
1312 if (!empty($settings['rewrite_form_actions'])) {
1313 $siteUrl = trailingslashit(get_site_url());
1314 $siteHost = parse_url($siteUrl, PHP_URL_HOST);
1315 $knownLangs = (isset($settings['languages']) && is_array($settings['languages'])) ? array_map('strtolower', $settings['languages']) : [$originalLang, $translatedLang];
1316 foreach ($htmlObj->find('form[action]') as $formNode) {
1317 $action = $formNode->action;
1318 $decodedAction = rawurldecode(html_entity_decode($action, ENT_QUOTES, 'UTF-8'));
1319
1320 // Skip non-rewritable actions
1321 if (preg_match('/^(mailto:|tel:|javascript:|#)/i', $decodedAction)) continue;
1322 if (preg_match('/\.php($|\?|\#)/i', $decodedAction)) continue;
1323 if (preg_match('/^https?:\/\//i', $decodedAction) && parse_url($decodedAction, PHP_URL_HOST) !== $siteHost) continue;
1324
1325 $queryString = '';
1326 if ($settings['ignore_querystring'] == 1 && strpos($decodedAction, '?') !== false) {
1327 $parts = explode('?', $decodedAction, 2);
1328 $decodedAction = $parts[0];
1329 $queryString = '?' . $parts[1];
1330 }
1331 $originalActionFull = $decodedAction . $queryString;
1332
1333 // Extract relative path
1334 $actionPath = $decodedAction;
1335 $isAbsolute = false;
1336 if (preg_match('/^https?:\/\//i', $actionPath)) {
1337 $isAbsolute = true;
1338 $actionPath = parse_url($actionPath, PHP_URL_PATH) ?: '/';
1339 }
1340
1341 // Add/replace language prefix
1342 $pathParts = explode('/', $actionPath);
1343 $langIndex = 1;
1344 if (isset($pathParts[$langIndex]) && $pathParts[$langIndex] === 'index.php') $langIndex = 2;
1345 if (!empty($settings['subfolder_installation'])) $langIndex = 2;
1346
1347 if (isset($pathParts[$langIndex]) && in_array(strtolower($pathParts[$langIndex]), $knownLangs)) {
1348 $pathParts[$langIndex] = $translatedLang;
1349 } else {
1350 array_splice($pathParts, $langIndex, 0, [$translatedLang]);
1351 }
1352
1353 $rebuiltPath = '/' . implode('/', array_filter($pathParts, function($p) { return $p !== ''; }));
1354 $rebuiltPath = gpt_trailingslashit_url($rebuiltPath);
1355 $computedAction = $isAbsolute ? (rtrim($siteUrl, '/') . $rebuiltPath) : $rebuiltPath;
1356 $computedAction = gpt_trailingslashit_url($computedAction);
1357
1358 // Try alias match
1359 $translatedAlias = null;
1360 if (!empty($translatedAliasesMap) || !empty($translatedAliasesRelativeMap)) {
1361 if (isset($translatedAliasesMap[$computedAction])) {
1362 $translatedAlias = $translatedAliasesMap[$computedAction];
1363 } elseif (isset($translatedAliasesRelativeMap[$rebuiltPath])) {
1364 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPath];
1365 } elseif (!$isAbsolute && !empty($settings['subfolder_installation'])) {
1366 // Fallback: form action is relative and includes subfolder path
1367 // Map keys strip the subfolder, so retry after stripping it
1368 $siteBasePath = rtrim(parse_url(rtrim($siteUrl, '/'), PHP_URL_PATH) ?: '', '/');
1369 if ($siteBasePath !== '' && strpos($rebuiltPath, $siteBasePath . '/') === 0) {
1370 $rebuiltPathNoSubfolder = substr($rebuiltPath, strlen($siteBasePath));
1371 if (isset($translatedAliasesRelativeMap[$rebuiltPathNoSubfolder])) {
1372 $translatedAlias = $translatedAliasesRelativeMap[$rebuiltPathNoSubfolder];
1373 }
1374 }
1375 }
1376 }
1377
1378 $finalAction = $translatedAlias ? ($translatedAlias . $queryString) : ($computedAction . $queryString);
1379
1380 if (!isset($formNode->{'data-originalaction'})) {
1381 $formNode->{'data-originalaction'} = $originalActionFull;
1382 }
1383 $formNode->action = $finalAction;
1384 }
1385 }
1386
1387 // Check if also images 'alt' are enabled to be translated
1388 if (! empty($settings['translate_altimages'])) {
1389 foreach ($htmlObj->find('img[alt]') as $imgNode) {
1390 $altText = $imgNode->alt;
1391 if ($matchQuotes) {
1392 $altText = str_ireplace('"', "'", $altText);
1393 }
1394 foreach ($altTranslationsArray as $originalText => $translatedText) {
1395 $originalText = normalizeText($originalText);
1396 $altText = normalizeText($altText);
1397
1398 $prevAltText = $altText;
1399 if ($caseInsensitive) {
1400 $altText = str_ireplace(trim($originalText), $translatedText, $altText);
1401 } else {
1402 $altText = str_replace(trim($originalText), $translatedText, $altText);
1403 }
1404 if ($altText !== $prevAltText) {
1405 break;
1406 }
1407 }
1408 $imgNode->alt = $altText;
1409 }
1410
1411 foreach ($htmlObj->find('img[title]') as $imgNode) {
1412 $titleText = $imgNode->title;
1413 if ($matchQuotes) {
1414 $titleText = str_ireplace('"', "'", $titleText);
1415 }
1416 foreach ($altTranslationsArray as $originalText => $translatedText) {
1417 $originalText = normalizeText($originalText);
1418 $titleText = normalizeText($titleText);
1419
1420 $prevTitleText = $titleText;
1421 if ($caseInsensitive) {
1422 $titleText = str_ireplace(trim($originalText), $translatedText, $titleText);
1423 } else {
1424 $titleText = str_replace(trim($originalText), $translatedText, $titleText);
1425 }
1426 if ($titleText !== $prevTitleText) {
1427 break;
1428 }
1429 }
1430 $imgNode->title = $titleText;
1431 }
1432
1433 // Check if also images 'src' are enabled to be translated
1434 if (! empty($settings['translate_srcimages'])) {
1435 foreach ($htmlObj->find('img[src]') as $imgNode) {
1436 $srcValue = ltrim($imgNode->src, '/');
1437
1438 if ($matchQuotes) {
1439 $srcValue = str_ireplace('"', "'", $srcValue);
1440 }
1441
1442 $translatedSrcValue = null;
1443 foreach ($altTranslationsArray as $originalText => $translatedText) {
1444 $originalText = normalizeText($originalText);
1445 $srcValue = normalizeText($srcValue);
1446
1447 $originalText = ltrim($originalText, '/');
1448 if ($caseInsensitive) {
1449 if (strcasecmp(trim($originalText), $srcValue) === 0) {
1450 $translatedSrcValue = $translatedText;
1451 break;
1452 }
1453 } else {
1454 if (trim($originalText) === $srcValue) {
1455 $translatedSrcValue = $translatedText;
1456 break;
1457 }
1458 }
1459 }
1460
1461 // If a translation was found, replace src and srcset
1462 if ($translatedSrcValue !== null) {
1463 // Replace the src attribute
1464 $imgNode->src = $translatedSrcValue;
1465
1466 // Check if srcset exists and replace it with the translated src value
1467 if (isset($imgNode->srcset)) {
1468 $imgNode->srcset = $translatedSrcValue;
1469 }
1470 }
1471 }
1472 }
1473 }
1474
1475 // Translate iframe locale parameter (e.g. Stripe payment forms)
1476 if (! empty($settings['translate_iframe_locale'])) {
1477 $currentLangCode = strtolower(explode('-', $currentLanguage)[0]);
1478 foreach ($htmlObj->find('iframe[src]') as $iframeNode) {
1479 $src = $iframeNode->src;
1480 if (strpos($src, 'locale=') !== false) {
1481 $iframeNode->src = preg_replace('/locale=[a-z]{2}/i', 'locale=' . $currentLangCode, $src);
1482 }
1483 }
1484 }
1485
1486 // Translate <meta name="description">
1487 foreach ($htmlObj->find('meta[name=description], meta[name=twitter:description], meta[property=og:description], meta[name=dc.description]') as $metaNode) {
1488 $contentValue = $metaNode->content;
1489
1490 // DECODIFICA HTML entities PRIMA del confronto
1491 $contentValue = html_entity_decode($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
1492
1493 if ($matchQuotes) {
1494 $contentValue = str_ireplace('"', "'", $contentValue);
1495 }
1496
1497 foreach ($altTranslationsArray as $originalText => $translatedText) {
1498 $prev = $contentValue;
1499 if ($caseInsensitive) {
1500 $contentValue = str_ireplace(trim($originalText), $translatedText, $contentValue);
1501 } else {
1502 $contentValue = str_replace(trim($originalText), $translatedText, $contentValue);
1503 }
1504 if ($contentValue !== $prev) break;
1505 }
1506
1507 // RI-ENCODIFICA per l'HTML output (sicurezza)
1508 $contentValue = htmlspecialchars($contentValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
1509
1510 $metaNode->content = $contentValue;
1511 }
1512
1513 foreach ($htmlObj->find('meta[property=og:title]') as $metaOgTitleNode) {
1514 // Find the <title> tag to get the translated title
1515 $titleNode = $htmlObj->find('title', 0);
1516
1517 if ($titleNode) {
1518 // Get the translated title text
1519 $translatedTitle = $titleNode->plaintext;
1520
1521 // Set og:title with the same value as <title>
1522 $metaOgTitleNode->content = trim($translatedTitle);
1523 }
1524 }
1525
1526 // Translate <meta name="twitter:title"> with the same value as <title> tag
1527 foreach ($htmlObj->find('meta[name=twitter:title]') as $metaTwitterTitleNode) {
1528 // Find the <title> tag to get the translated title
1529 $titleNode = $htmlObj->find('title', 0);
1530
1531 if ($titleNode) {
1532 // Get the translated title text
1533 $translatedTitle = $titleNode->plaintext;
1534
1535 // Set twitter:title with the same value as <title>
1536 $metaTwitterTitleNode->content = trim($translatedTitle);
1537 }
1538 }
1539
1540 // Translate <meta name="dc:title"> (Dublin Core - SEOPress) with the same value as <title> tag
1541 foreach ($htmlObj->find('meta[name=dc.title]') as $metaDcTitleNode) {
1542 // Find the <title> tag to get the translated title
1543 $titleNode = $htmlObj->find('title', 0);
1544
1545 if ($titleNode) {
1546 // Get the translated title text
1547 $translatedTitle = $titleNode->plaintext;
1548
1549 // Set dc:title with the same value as <title>
1550 $metaDcTitleNode->content = trim($translatedTitle);
1551 }
1552 }
1553
1554 if ($bodyElement = $htmlObj->find('body', 0)) {
1555 $bodyElement->setAttribute('data-gptranslateskip', '1');
1556 $bodyElement->setAttribute ( 'data-gptranslateoriginalalias', htmlspecialchars($row['pagelink'], ENT_QUOTES, 'UTF-8') );
1557 }
1558
1559 $modifiedHtml = $htmlObj->save();
1560 $modifiedHtml = html_entity_decode($modifiedHtml, ENT_QUOTES | ENT_HTML5, 'UTF-8');
1561
1562 $html = $modifiedHtml;
1563 } catch (Exception $e) {
1564 // Handle exceptions as needed
1565 }
1566 } elseif ($settings['serverside_translations_method'] == 'strireplace') {
1567 // Solution 3: simplest approach, unconditional str_ireplace that could cause unintentional replacements
1568 try {
1569 $translationsArray = json_decode($row['translations'], true) ?? [];
1570 $altTranslationsArray = json_decode($row['alt_translations'] ?? '', true) ?: [];
1571
1572 // Do body page translations replacements
1573 uksort($translationsArray, function ($a, $b) {
1574 return strlen($b) - strlen($a);
1575 });
1576
1577 $caseInsensitive = !empty($settings['serverside_translations_caseinsensitive']);
1578 $matchQuotes = !empty($settings['serverside_translations_matchquotes']);
1579
1580 foreach ($translationsArray as $originalText => $translatedText) {
1581 $originalText = normalizeText($originalText);
1582 $html = normalizeText($html);
1583
1584 if ($caseInsensitive) {
1585 $html = str_ireplace(trim($originalText), $translatedText, $html);
1586
1587 // Check also if both single quotes or double quotes should be checked to replace
1588 if ($matchQuotes && strpos($originalText, "'") !== false) {
1589 $originalTextAlt = str_ireplace("'", '"', $originalText);
1590 $html = str_ireplace(trim($originalTextAlt), $translatedText, $html);
1591 }
1592 } else {
1593 $html = str_replace(trim($originalText), $translatedText, $html);
1594
1595 if ($matchQuotes && strpos($originalText, "'") !== false) {
1596 $originalTextAlt = str_replace("'", '"', $originalText);
1597 $html = str_replace(trim($originalTextAlt), $translatedText, $html);
1598 }
1599 }
1600 }
1601
1602 // Check if also images 'alt' are enabled to be translated
1603 if (!empty($settings['translate_altimages'])) {
1604 foreach ($altTranslationsArray as $originalAlt => $translatedAlt) {
1605 if ($matchQuotes) {
1606 $originalAlt = str_ireplace('"', "'", $originalAlt);
1607 }
1608 if ($caseInsensitive) {
1609 $html = str_ireplace('alt="' . trim($originalAlt) . '"', 'alt="' . $translatedAlt . '"', $html);
1610 $html = str_ireplace("alt='" . trim($originalAlt) . "'", "alt='" . $translatedAlt . "'", $html);
1611 } else {
1612 $html = str_replace('alt="' . trim($originalAlt) . '"', 'alt="' . $translatedAlt . '"', $html);
1613 $html = str_replace("alt='" . trim($originalAlt) . "'", "alt='" . $translatedAlt . "'", $html);
1614 }
1615 }
1616 }
1617
1618 $html = str_ireplace('<body', '<body data-gptranslateskip="1" data-gptranslateoriginalalias="' . $row['pagelink'] . '"', $html);
1619
1620 } catch (Exception $e) {
1621 // Handle exceptions as needed
1622 }
1623 }
1624
1625 return $html;
1626 } );
1627 } );
1628