PluginProbe
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI / 2.34.5
GPTranslate – Multilingual AI Translation Agent for WordPress: Translate Your Site with AI v2.34.5
2.34.5 2.34.4 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 All 57 releases
gptranslate / multilang-routing.php
multilang-routing.php
1,435 lines 53.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GPTranslate - Multilang Routing System for WordPress
4 * Handles language prefixes in URLs like /it/, /fr/, /en/, etc.
5 * and maps them to internal pages, similar to Joomla LanguageFilter plugin.
6 */
7
8 if (!defined('ABSPATH')) exit;
9
10 // Load only in frontend
11 if (! is_admin ()) {
12 $settings = get_option("gptranslate_options");
13
14 if ( ! function_exists( 'is_plugin_active' ) ) {
15 include_once ABSPATH . 'wp-admin/includes/plugin.php';
16 }
17 $polylangActive = is_plugin_active( 'polylang/polylang.php' );
18
19 $enabledLanguages = $settings['languages'] ?? [];
20
21 // Define supported languages (lowercase)
22 $gpt_supported_languages = array_map ( 'strtolower', [
23 'AF',
24 'SQ',
25 'AM',
26 'AR',
27 'HY',
28 'AZ',
29 'EU',
30 'BE',
31 'BN',
32 'BS',
33 'BG',
34 'CA',
35 'CEB',
36 'NY',
37 'ZH',
38 'CO',
39 'HR',
40 'CS',
41 'DA',
42 'NL',
43 'EN',
44 'EO',
45 'ET',
46 'TL',
47 'FI',
48 'FR',
49 'FY',
50 'GL',
51 'KA',
52 'DE',
53 'EL',
54 'GU',
55 'HT',
56 'HA',
57 'HAW',
58 'IW',
59 'HI',
60 'HMN',
61 'HU',
62 'IS',
63 'IG',
64 'ID',
65 'GA',
66 'IT',
67 'JA',
68 'JW',
69 'KN',
70 'KK',
71 'KM',
72 'KO',
73 'KU',
74 'KY',
75 'LO',
76 'LA',
77 'LV',
78 'LT',
79 'LB',
80 'MK',
81 'MG',
82 'MS',
83 'ML',
84 'MT',
85 'MI',
86 'MR',
87 'MN',
88 'MY',
89 'NE',
90 'NO',
91 'PS',
92 'FA',
93 'PL',
94 'PT',
95 'PA',
96 'RO',
97 'RU',
98 'SM',
99 'GD',
100 'SR',
101 'ST',
102 'SN',
103 'SD',
104 'SI',
105 'SK',
106 'SL',
107 'SO',
108 'ES',
109 'SU',
110 'SW',
111 'SV',
112 'TG',
113 'TA',
114 'TE',
115 'TH',
116 'TR',
117 'UK',
118 'UR',
119 'UZ',
120 'VI',
121 'CY',
122 'XH',
123 'YI',
124 'YO',
125 'ZU',
126 'ZT'
127 ] );
128
129 // iso => custom slug, built once and reused both to remap the full reference list below
130 // (still used elsewhere for outbound link rewriting) and to remap the routable subset.
131 $gpt_custom_prefix_map = [];
132 foreach (explode(',', $settings['custom_url_prefixes'] ?? '') as $mapping) {
133 $parts = explode(':', trim($mapping));
134 if (count($parts) === 2) {
135 $from = strtolower(trim($parts[0]));
136 $to = strtolower(trim($parts[1]));
137 if ($from && $to) {
138 $gpt_custom_prefix_map[$from] = $to;
139 }
140 }
141 }
142 foreach ($gpt_custom_prefix_map as $from => $to) {
143 $key = array_search($from, $gpt_supported_languages);
144 if ($key !== false) $gpt_supported_languages[$key] = $to;
145 }
146
147 // The routable set used for inbound URL matching (language-prefix detection, WP rewrite
148 // rules): unlike $gpt_supported_languages above (every language the plugin can ever
149 // translate to, used only for rewriting outbound links), this must be restricted to the
150 // languages actually enabled on this site. Otherwise WordPress recognizes and serves
151 // content under ANY of the ~100 supported prefixes (e.g. /hu/, /ro/, /no/) even when the
152 // admin only enabled a subset, returning 200 with duplicate content instead of a 404 -
153 // generating "duplicate page, Google chose a different canonical" issues in Search Console.
154 $enabledLanguagesLower = array_map('strtolower', $enabledLanguages);
155 $sourceLangLower = strtolower($settings['language'] ?? 'en');
156 if ($sourceLangLower && !in_array($sourceLangLower, $enabledLanguagesLower, true)) {
157 $enabledLanguagesLower[] = $sourceLangLower;
158 }
159 $gpt_routable_languages = array_map(function ($code) use ($gpt_custom_prefix_map) {
160 return $gpt_custom_prefix_map[$code] ?? $code;
161 }, $enabledLanguagesLower);
162 // Fallback to the full list only if somehow no language is configured, for safety/back-compat
163 if (empty($gpt_routable_languages)) {
164 $gpt_routable_languages = $gpt_supported_languages;
165 }
166
167 /**
168 * GPTranslate - Alias Translation Routing for WordPress
169 * This section augments the language prefix routing by checking translated aliases
170 * and routing them to the original page link.
171 */
172
173 add_action('init', function () use ($gpt_routable_languages, $polylangActive, $settings) {
174 if (is_admin()) return;
175
176 global $wpdb;
177
178 $subfolder = $settings['subfolder_installation'] ?? 0;
179 $rewriteAlias = $settings['rewrite_language_alias'] ?? 0;
180 $rewriteUrl = $settings['rewrite_language_url'] ?? 0;
181 $originalLang = $settings['language'] ?? 'en';
182
183 if (! $rewriteAlias || ! $rewriteUrl) return;
184
185 $requestUri = urldecode($_SERVER['REQUEST_URI']) ?? '';
186 $requestUri = sanitize_text_field(wp_unslash($requestUri));
187
188 // Remove subfolder if present
189 if ($subfolder) {
190 $homePath = wp_parse_url(home_url(), PHP_URL_PATH) ?? '';
191 if ($homePath && strpos($requestUri, $homePath) === 0) {
192 $requestUri = substr($requestUri, strlen($homePath));
193 }
194 }
195
196 // Normalize path
197 $rawUri = rtrim(home_url() . $requestUri, '/');
198 $rawUriWithSlash = $rawUri . '/';
199
200 $uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
201 // Only languages actually enabled on this site are recognized as a routable prefix -
202 // see $gpt_routable_languages definition above.
203 $pattern = '#^/(' . implode ( '|', array_map ( 'preg_quote', $gpt_routable_languages ) ) . ')(/.*)?$#i';
204
205 // Optional: remove subfolder if set
206 $subfolder_prefix = ''; // To hold subfolder if any
207 if ($settings['subfolder_installation']) {
208 $uri_parts = explode('/', ltrim($uri, '/'));
209
210 // Remove the first part (subfolder name)
211 $subfolder_prefix = '/' . array_shift($uri_parts);
212
213 // Rebuild URI without subfolder
214 $uri_to_match = '/' . implode('/', $uri_parts);
215 } else {
216 $uri_to_match = $uri;
217 }
218
219 if (preg_match ( $pattern, $uri_to_match, $matches )) {
220 $currentLang = strtolower ( $matches [1] );
221 } else {
222 return;
223 }
224
225 // Check translated alias
226 $table = $wpdb->prefix . 'gptranslate';
227 $query = $wpdb->prepare(
228 "SELECT pagelink FROM $table WHERE published = 1 AND languagetranslated = %s AND (
229 translated_alias = %s OR translated_alias = %s)",
230 $currentLang, $rawUri, $rawUriWithSlash );
231
232 $originalPageLink = $wpdb->get_var($query);
233
234 if (! $originalPageLink) {
235 // The request didn't match a translated alias, so it may be the original-language
236 // slug accessed directly under a translated language prefix (e.g. /hu/original-slug
237 // instead of /hu/translated-slug). Both would otherwise serve identical content with
238 // a 200 OK, causing duplicate indexing, so redirect to the canonical translated alias.
239 if (! empty($settings['redirect_alias_to_translated'])) {
240 $redirectQuery = $wpdb->prepare(
241 "SELECT translated_alias FROM $table WHERE published = 1 AND languagetranslated = %s AND (
242 pagelink = %s OR pagelink = %s) AND translated_alias IS NOT NULL AND translated_alias != ''",
243 $currentLang, $rawUri, $rawUriWithSlash );
244
245 $translatedAlias = $wpdb->get_var($redirectQuery);
246
247 if ($translatedAlias) {
248 wp_safe_redirect($translatedAlias, 301);
249 exit;
250 }
251 }
252 return;
253 }
254
255 // Extract path from URL
256 $parsed = wp_parse_url($originalPageLink);
257 $originalPath = $parsed['path'] ?? '';
258
259 // Remove subfolder prefix again from final path
260 if ($subfolder && !empty($homePath) && strpos($originalPath, $homePath) === 0) {
261 $originalPath = substr($originalPath, strlen($homePath));
262 define('GPTRANSLATE_SUBFOLDER_REPLACED', true);
263 }
264
265 // Clean path
266 $originalPath = preg_replace('#^/?index\.php/?#i', '', $originalPath);
267 $originalPath = '/' . ltrim($originalPath, '/');
268
269 // Preserve the public URL (as requested by the user) before rewriting to original WP path
270 if (empty($GLOBALS['gptranslate_public_request_uri'])) {
271 $GLOBALS['gptranslate_public_request_uri'] = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
272 }
273
274 // Rewrite the request
275 $_SERVER['REQUEST_URI'] = $originalPath;
276 }, 1);
277
278 /**
279 * Detect language from URI and rewrite $_SERVER['REQUEST_URI'] accordingly defining a GPTRANSLATE_CURRENT_LANG constant
280 */
281 add_action ( 'init', function () use ($gpt_routable_languages, $polylangActive, $settings) {
282 if (is_admin()) return;
283
284 $uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
285 // Only languages actually enabled on this site are recognized as a routable prefix -
286 // see $gpt_routable_languages definition above.
287 $pattern = '#^/(' . implode ( '|', array_map ( 'preg_quote', $gpt_routable_languages ) ) . ')(/.*)?$#i';
288
289 // Optional: remove subfolder if set
290 $subfolder_prefix = ''; // To hold subfolder if any, avoid double removal
291 if ($settings['subfolder_installation'] && !defined('GPTRANSLATE_SUBFOLDER_REPLACED')) {
292 $uri_parts = explode('/', ltrim($uri, '/'));
293
294 // Remove the first part (subfolder name)
295 $subfolder_prefix = '/' . array_shift($uri_parts);
296
297 // Rebuild URI without subfolder
298 $uri_to_match = '/' . implode('/', $uri_parts);
299 } else {
300 $uri_to_match = $uri;
301 }
302
303 if (preg_match ( $pattern, $uri_to_match, $matches )) {
304 $lang = strtolower ( $matches [1] );
305 $new_uri = isset ( $matches [2] ) ? $matches [2] : '/';
306 if ($settings ['rewrite_language_url'] == 1 && !$polylangActive) {
307 // Preserve the public URL as requested by the browser before stripping the language prefix.
308 // Covers the case where the alias-lookup hook (priority 1) found no matching translated_alias
309 // and returned early without capturing it, which otherwise makes canonical/hreflang fall back
310 // incorrectly to the default language URL.
311 if (empty($GLOBALS['gptranslate_public_request_uri'])) {
312 $GLOBALS['gptranslate_public_request_uri'] = $uri;
313 }
314 $_SERVER ['REQUEST_URI'] = $subfolder_prefix . $new_uri;
315 }
316
317 if (! defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
318 define ( 'GPTRANSLATE_CURRENT_LANG', $lang );
319 }
320 }
321 }, 2);
322
323 /**
324 * Replace and manage the lang attribute of the html tag
325 */
326 add_filter ( 'language_attributes', function ($output) use ($settings, $polylangActive) {
327 if (! defined ( 'GPTRANSLATE_CURRENT_LANG' ) || (!$settings['set_html_lang'] && !$settings ['rewrite_language_url']) || $polylangActive)
328 return $output;
329
330 $altflags_array = get_option ( 'gptranslate_options' ) ['alt_flags'] ?? [ ];
331
332 $lang_map = [
333 // Variazioni per EN
334 'en' => (in_array ( 'usa', $altflags_array ) || in_array ( 'canada', $altflags_array )) ? 'en-US' : 'en-GB',
335
336 // Variazioni per FR
337 'fr' => in_array ( 'quebec', $altflags_array ) ? 'fr-CA' : 'fr-FR',
338
339 // Variazioni per PT
340 'pt' => in_array ( 'brazil', $altflags_array ) ? 'pt-BR' : 'pt-PT',
341
342 // Variazioni per ES
343 'es' => in_array ( 'mexico', $altflags_array ) ? 'es-MX' : (in_array ( 'argentina', $altflags_array ) ? 'es-AR' : (in_array ( 'colombia', $altflags_array ) ? 'es-CO' : 'es-ES')),
344
345 // Tutte le altre senza variazioni
346 'af' => 'af-ZA',
347 'sq' => 'sq-AL',
348 'am' => 'am-ET',
349 'ar' => 'ar-SA',
350 'hy' => 'hy-AM',
351 'az' => 'az-AZ',
352 'eu' => 'eu-ES',
353 'be' => 'be-BY',
354 'bn' => 'bn-BD',
355 'bs' => 'bs-BA',
356 'bg' => 'bg-BG',
357 'ca' => 'ca-ES',
358 'ceb' => 'ceb-PH',
359 'ny' => 'ny-MW',
360 'zh' => 'zh-CN',
361 'zt' => 'zh-TW',
362 'co' => 'co-FR',
363 'hr' => 'hr-HR',
364 'cs' => 'cs-CZ',
365 'da' => 'da-DK',
366 'nl' => 'nl-NL',
367 'eo' => 'eo-EO',
368 'et' => 'et-EE',
369 'tl' => 'tl-PH',
370 'fi' => 'fi-FI',
371 'fy' => 'fy-NL',
372 'gl' => 'gl-ES',
373 'ka' => 'ka-GE',
374 'de' => 'de-DE',
375 'el' => 'el-GR',
376 'gu' => 'gu-IN',
377 'ht' => 'ht-HT',
378 'ha' => 'ha-NG',
379 'haw' => 'haw-US',
380 'iw' => 'iw-IL',
381 'hi' => 'hi-IN',
382 'hmn' => 'hmn-US',
383 'hu' => 'hu-HU',
384 'is' => 'is-IS',
385 'ig' => 'ig-NG',
386 'id' => 'id-ID',
387 'ga' => 'ga-IE',
388 'it' => 'it-IT',
389 'ja' => 'ja-JP',
390 'jw' => 'jw-ID',
391 'kn' => 'kn-IN',
392 'kk' => 'kk-KZ',
393 'km' => 'km-KH',
394 'ko' => 'ko-KR',
395 'ku' => 'ku-TR',
396 'ky' => 'ky-KG',
397 'lo' => 'lo-LA',
398 'la' => 'la-VA',
399 'lv' => 'lv-LV',
400 'lt' => 'lt-LT',
401 'lb' => 'lb-LU',
402 'mk' => 'mk-MK',
403 'mg' => 'mg-MG',
404 'ms' => 'ms-MY',
405 'ml' => 'ml-IN',
406 'mt' => 'mt-MT',
407 'mi' => 'mi-NZ',
408 'mr' => 'mr-IN',
409 'mn' => 'mn-MN',
410 'my' => 'my-MM',
411 'ne' => 'ne-NP',
412 'no' => 'no-NO',
413 'ps' => 'ps-AF',
414 'fa' => 'fa-IR',
415 'pl' => 'pl-PL',
416 'pa' => 'pa-IN',
417 'ro' => 'ro-RO',
418 'ru' => 'ru-RU',
419 'sm' => 'sm-WS',
420 'gd' => 'gd-GB',
421 'sr' => 'sr-RS',
422 'st' => 'st-LS',
423 'sn' => 'sn-ZW',
424 'sd' => 'sd-PK',
425 'si' => 'si-LK',
426 'sk' => 'sk-SK',
427 'sl' => 'sl-SI',
428 'so' => 'so-SO',
429 'su' => 'su-ID',
430 'sw' => 'sw-TZ',
431 'sv' => 'sv-SE',
432 'tg' => 'tg-TJ',
433 'ta' => 'ta-IN',
434 'te' => 'te-IN',
435 'th' => 'th-TH',
436 'tr' => 'tr-TR',
437 'uk' => 'uk-UA',
438 'ur' => 'ur-PK',
439 'uz' => 'uz-UZ',
440 'vi' => 'vi-VN',
441 'cy' => 'cy-GB',
442 'xh' => 'xh-ZA',
443 'yi' => 'yi-US',
444 'yo' => 'yo-NG',
445 'zu' => 'zu-ZA'
446 ];
447
448 $gpt_lang = strtolower ( GPTRANSLATE_CURRENT_LANG );
449 $full_lang = $lang_map [$gpt_lang] ?? $gpt_lang;
450
451 $output = 'lang="' . esc_attr ( $full_lang ) . '"';
452
453 // --- RTL support: set WP_Locale direction dynamically ---
454 global $wp_locale;
455 $rtl_languages = array("ar", "fa", "iw", "ps", "sd", "ur", "yi"); // Arabic, Hebrew, Persian, Urdu
456 if ( (int)$settings['auto_set_language_direction'] && in_array( $gpt_lang, $rtl_languages, true ) ) {
457 $wp_locale->text_direction = 'rtl';
458 // ensure HTML dir="rtl"
459 if ( strpos( $output, 'dir=' ) === false ) {
460 $output .= ' dir="rtl"';
461 } else {
462 $output = preg_replace( '/dir="ltr"/', 'dir="rtl"', $output );
463 }
464 return $output;
465 }
466
467 return $output;
468 } );
469
470 /**
471 * Replace og:locale meta tag to match current language
472 */
473 add_filter('wpseo_og_locale', function($locale) use ($settings, $polylangActive) {
474 if (!defined('GPTRANSLATE_CURRENT_LANG') || (!$settings['set_html_lang'] && !$settings['rewrite_language_url']) || $polylangActive) {
475 return $locale;
476 }
477
478 $altflags_array = get_option('gptranslate_options')['alt_flags'] ?? [];
479
480 $lang_map = [
481 'en' => (in_array('usa', $altflags_array) || in_array('canada', $altflags_array)) ? 'en-US' : 'en-GB',
482 'fr' => in_array('quebec', $altflags_array) ? 'fr-CA' : 'fr-FR',
483 'pt' => in_array('brazil', $altflags_array) ? 'pt-BR' : 'pt-PT',
484 'es' => in_array('mexico', $altflags_array) ? 'es-MX' : (in_array('argentina', $altflags_array) ? 'es-AR' : (in_array('colombia', $altflags_array) ? 'es-CO' : 'es-ES')),
485 'af' => 'af-ZA', 'sq' => 'sq-AL', 'am' => 'am-ET', 'ar' => 'ar-SA', 'hy' => 'hy-AM',
486 'az' => 'az-AZ', 'eu' => 'eu-ES', 'be' => 'be-BY', 'bn' => 'bn-BD', 'bs' => 'bs-BA',
487 'bg' => 'bg-BG', 'ca' => 'ca-ES', 'ceb' => 'ceb-PH', 'ny' => 'ny-MW', 'zh' => 'zh-CN',
488 'zt' => 'zh-TW', 'co' => 'co-FR', 'hr' => 'hr-HR', 'cs' => 'cs-CZ', 'da' => 'da-DK',
489 'nl' => 'nl-NL', 'eo' => 'eo-EO', 'et' => 'et-EE', 'tl' => 'tl-PH', 'fi' => 'fi-FI',
490 'fy' => 'fy-NL', 'gl' => 'gl-ES', 'ka' => 'ka-GE', 'de' => 'de-DE', 'el' => 'el-GR',
491 'gu' => 'gu-IN', 'ht' => 'ht-HT', 'ha' => 'ha-NG', 'haw' => 'haw-US', 'iw' => 'iw-IL',
492 'hi' => 'hi-IN', 'hmn' => 'hmn-US', 'hu' => 'hu-HU', 'is' => 'is-IS', 'ig' => 'ig-NG',
493 'id' => 'id-ID', 'ga' => 'ga-IE', 'it' => 'it-IT', 'ja' => 'ja-JP', 'jw' => 'jw-ID',
494 'kn' => 'kn-IN', 'kk' => 'kk-KZ', 'km' => 'km-KH', 'ko' => 'ko-KR', 'ku' => 'ku-TR',
495 'ky' => 'ky-KG', 'lo' => 'lo-LA', 'la' => 'la-VA', 'lv' => 'lv-LV', 'lt' => 'lt-LT',
496 'lb' => 'lb-LU', 'mk' => 'mk-MK', 'mg' => 'mg-MG', 'ms' => 'ms-MY', 'ml' => 'ml-IN',
497 'mt' => 'mt-MT', 'mi' => 'mi-NZ', 'mr' => 'mr-IN', 'mn' => 'mn-MN', 'my' => 'my-MM',
498 'ne' => 'ne-NP', 'no' => 'no-NO', 'ps' => 'ps-AF', 'fa' => 'fa-IR', 'pl' => 'pl-PL',
499 'pa' => 'pa-IN', 'ro' => 'ro-RO', 'ru' => 'ru-RU', 'sm' => 'sm-WS', 'gd' => 'gd-GB',
500 'sr' => 'sr-RS', 'st' => 'st-LS', 'sn' => 'sn-ZW', 'sd' => 'sd-PK', 'si' => 'si-LK',
501 'sk' => 'sk-SK', 'sl' => 'sl-SI', 'so' => 'so-SO', 'su' => 'su-ID', 'sw' => 'sw-TZ',
502 'sv' => 'sv-SE', 'tg' => 'tg-TJ', 'ta' => 'ta-IN', 'te' => 'te-IN', 'th' => 'th-TH',
503 'tr' => 'tr-TR', 'uk' => 'uk-UA', 'ur' => 'ur-PK', 'uz' => 'uz-UZ', 'vi' => 'vi-VN',
504 'cy' => 'cy-GB', 'xh' => 'xh-ZA', 'yi' => 'yi-US', 'yo' => 'yo-NG', 'zu' => 'zu-ZA'
505 ];
506
507 $gpt_lang = strtolower(GPTRANSLATE_CURRENT_LANG);
508 $full_lang = $lang_map[$gpt_lang] ?? $gpt_lang;
509
510 // Convert format from xx-YY to xx_YY for og:locale (underscore instead of dash)
511 return str_replace('-', '_', $full_lang);
512 }, 99);
513
514 // Support for RankMath SEO
515 add_filter('rank_math/opengraph/locale', function($locale) use ($settings, $polylangActive) {
516 if (!defined('GPTRANSLATE_CURRENT_LANG') || (!$settings['set_html_lang'] && !$settings['rewrite_language_url']) || $polylangActive) {
517 return $locale;
518 }
519
520 $altflags_array = get_option('gptranslate_options')['alt_flags'] ?? [];
521
522 $lang_map = [
523 'en' => (in_array('usa', $altflags_array) || in_array('canada', $altflags_array)) ? 'en-US' : 'en-GB',
524 'fr' => in_array('quebec', $altflags_array) ? 'fr-CA' : 'fr-FR',
525 'pt' => in_array('brazil', $altflags_array) ? 'pt-BR' : 'pt-PT',
526 'es' => in_array('mexico', $altflags_array) ? 'es-MX' : (in_array('argentina', $altflags_array) ? 'es-AR' : (in_array('colombia', $altflags_array) ? 'es-CO' : 'es-ES')),
527 'af' => 'af-ZA', 'sq' => 'sq-AL', 'am' => 'am-ET', 'ar' => 'ar-SA', 'hy' => 'hy-AM',
528 'az' => 'az-AZ', 'eu' => 'eu-ES', 'be' => 'be-BY', 'bn' => 'bn-BD', 'bs' => 'bs-BA',
529 'bg' => 'bg-BG', 'ca' => 'ca-ES', 'ceb' => 'ceb-PH', 'ny' => 'ny-MW', 'zh' => 'zh-CN',
530 'zt' => 'zh-TW', 'co' => 'co-FR', 'hr' => 'hr-HR', 'cs' => 'cs-CZ', 'da' => 'da-DK',
531 'nl' => 'nl-NL', 'eo' => 'eo-EO', 'et' => 'et-EE', 'tl' => 'tl-PH', 'fi' => 'fi-FI',
532 'fy' => 'fy-NL', 'gl' => 'gl-ES', 'ka' => 'ka-GE', 'de' => 'de-DE', 'el' => 'el-GR',
533 'gu' => 'gu-IN', 'ht' => 'ht-HT', 'ha' => 'ha-NG', 'haw' => 'haw-US', 'iw' => 'iw-IL',
534 'hi' => 'hi-IN', 'hmn' => 'hmn-US', 'hu' => 'hu-HU', 'is' => 'is-IS', 'ig' => 'ig-NG',
535 'id' => 'id-ID', 'ga' => 'ga-IE', 'it' => 'it-IT', 'ja' => 'ja-JP', 'jw' => 'jw-ID',
536 'kn' => 'kn-IN', 'kk' => 'kk-KZ', 'km' => 'km-KH', 'ko' => 'ko-KR', 'ku' => 'ku-TR',
537 'ky' => 'ky-KG', 'lo' => 'lo-LA', 'la' => 'la-VA', 'lv' => 'lv-LV', 'lt' => 'lt-LT',
538 'lb' => 'lb-LU', 'mk' => 'mk-MK', 'mg' => 'mg-MG', 'ms' => 'ms-MY', 'ml' => 'ml-IN',
539 'mt' => 'mt-MT', 'mi' => 'mi-NZ', 'mr' => 'mr-IN', 'mn' => 'mn-MN', 'my' => 'my-MM',
540 'ne' => 'ne-NP', 'no' => 'no-NO', 'ps' => 'ps-AF', 'fa' => 'fa-IR', 'pl' => 'pl-PL',
541 'pa' => 'pa-IN', 'ro' => 'ro-RO', 'ru' => 'ru-RU', 'sm' => 'sm-WS', 'gd' => 'gd-GB',
542 'sr' => 'sr-RS', 'st' => 'st-LS', 'sn' => 'sn-ZW', 'sd' => 'sd-PK', 'si' => 'si-LK',
543 'sk' => 'sk-SK', 'sl' => 'sl-SI', 'so' => 'so-SO', 'su' => 'su-ID', 'sw' => 'sw-TZ',
544 'sv' => 'sv-SE', 'tg' => 'tg-TJ', 'ta' => 'ta-IN', 'te' => 'te-IN', 'th' => 'th-TH',
545 'tr' => 'tr-TR', 'uk' => 'uk-UA', 'ur' => 'ur-PK', 'uz' => 'uz-UZ', 'vi' => 'vi-VN',
546 'cy' => 'cy-GB', 'xh' => 'xh-ZA', 'yi' => 'yi-US', 'yo' => 'yo-NG', 'zu' => 'zu-ZA'
547 ];
548
549 $gpt_lang = strtolower(GPTRANSLATE_CURRENT_LANG);
550 $full_lang = $lang_map[$gpt_lang] ?? $gpt_lang;
551
552 // Convert format from xx-YY to xx_YY for og:locale
553 return str_replace('-', '_', $full_lang);
554 }, 99);
555
556 /**
557 * Inject alternate hreflang links in <head>
558 */
559 add_action('wp_head', function () use ($enabledLanguages, $polylangActive, $settings) {
560 if (!$settings['add_alternate'] || $polylangActive) {
561 return;
562 }
563
564 global $wpdb;
565
566 // Se GPTRANSLATE_CURRENT_LANG non e' definita, usa la lingua originale
567 // Questo succede quando omit_prefix_original_language e' attivo e visiti /sample-page/ senza /en/
568 if (!defined('GPTRANSLATE_CURRENT_LANG')) {
569 $current_lang = $settings['language']; // Usa lingua originale come fallback
570 } else {
571 $current_lang = GPTRANSLATE_CURRENT_LANG;
572 }
573
574 $original_lang = $settings['language']; // x-default language
575 $omit_prefix = !empty($settings['omit_prefix_original_language']); // Check if prefix should be omitted
576 $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
577 $request_path = sanitize_text_field(wp_unslash($request_uri));
578
579 // Remove query string
580 $request_path = strtok($request_path, '?');
581
582 // Optional subfolder cleanup
583 if (!empty($settings['subfolder_installation'])) {
584 $home_path = wp_parse_url(home_url(), PHP_URL_PATH) ?? '';
585 if ($home_path && strpos($request_path, $home_path) === 0) {
586 $request_path = substr($request_path, strlen($home_path));
587 }
588 }
589
590 // Build current full URL
591 $site_url = home_url();
592 $current_full_url = rtrim($site_url, '/') . $request_path;
593
594 // Normalize URL with trailing slash
595 $current_full_url_normalized = gpt_trailingslashit_url($current_full_url);
596
597 // STEP 1: Determina URL originale (senza slug lingua)
598 $original_url_no_lang = null;
599
600 if ($current_lang === $original_lang && $omit_prefix) {
601 // Siamo sulla lingua originale e prefix omesso
602 $original_url_no_lang = $current_full_url_normalized;
603 } else {
604 // Rimuovi lo slug lingua dall'URL corrente per ottenere URL base
605 $current_path_no_lang = preg_replace('#^/' . preg_quote($current_lang, '#') . '(/|$)#', '/', $request_path);
606 $original_url_no_lang = gpt_trailingslashit_url(rtrim($site_url, '/') . $current_path_no_lang);
607 }
608
609 // STEP 2: Per ogni lingua, cerca translated_alias nel DB
610 $table_name = $wpdb->prefix . 'gptranslate';
611 $altflags_array = get_option('gptranslate_options')['alt_flags'] ?? [];
612 $altflags_array = is_array($altflags_array) ? array_map('strtolower', $altflags_array) : [];
613 $emitted_localized_hreflangs = [];
614 $get_localized_hreflang = static function($lang) use ($altflags_array) {
615 $lang = strtolower((string) $lang);
616 if ($lang === 'en') {
617 if (in_array('usa', $altflags_array, true)) return 'en-US';
618 if (in_array('canada', $altflags_array, true)) return 'en-CA';
619 if (in_array('ireland', $altflags_array, true)) return 'en-IE';
620 } elseif ($lang === 'pt') {
621 if (in_array('brazil', $altflags_array, true)) return 'pt-BR';
622 } elseif ($lang === 'es') {
623 if (in_array('mexico', $altflags_array, true)) return 'es-MX';
624 if (in_array('argentina', $altflags_array, true)) return 'es-AR';
625 if (in_array('colombia', $altflags_array, true)) return 'es-CO';
626 } elseif ($lang === 'fr') {
627 if (in_array('quebec', $altflags_array, true)) return 'fr-CA';
628 } elseif ($lang === 'de') {
629 if (in_array('austria', $altflags_array, true)) return 'de-AT';
630 } elseif ($lang === 'zh') {
631 if (in_array('taiwan', $altflags_array, true)) return 'zh-TW';
632 if (in_array('hongkong', $altflags_array, true)) return 'zh-HK';
633 } elseif ($lang === 'zt') {
634 if (in_array('hongkong', $altflags_array, true)) return 'zh-HK';
635 if (in_array('taiwan', $altflags_array, true)) return 'zh-TW';
636 }
637
638 return null;
639 };
640
641 foreach ($enabledLanguages as $lang) {
642 $hreflang_url = null;
643
644 if ($lang === $original_lang) {
645 // Lingua originale
646 if ($omit_prefix) {
647 // Prefix omesso: URL senza slug lingua
648 $hreflang_url = $original_url_no_lang;
649 } else {
650 // Prefix NON omesso: URL con slug lingua
651 $original_path_only = parse_url($original_url_no_lang, PHP_URL_PATH);
652
653 // Remove subfolder from path if needed
654 if (!empty($settings['subfolder_installation'])) {
655 $home_path = wp_parse_url(home_url(), PHP_URL_PATH) ?? '';
656 if ($home_path && strpos($original_path_only, $home_path) === 0) {
657 $original_path_only = substr($original_path_only, strlen($home_path));
658 }
659 }
660
661 $pagelink_with_lang = rtrim($site_url, '/') . '/' . $lang . rtrim($original_path_only, '/') . '/';
662
663 // Prepare decoded version for URL matching
664 $pagelink_decoded = urldecode($pagelink_with_lang);
665
666 // Query per trovare translated_alias con varianti (con/senza trailing slash + encoded/decoded)
667 $result = $wpdb->get_row($wpdb->prepare(
668 "SELECT translated_alias FROM {$table_name}
669 WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s)
670 AND languageoriginal = %s
671 AND languagetranslated = %s
672 AND published = 1
673 LIMIT 1",
674 rtrim($pagelink_with_lang, '/'), // senza trailing slash (encoded)
675 rtrim($pagelink_with_lang, '/') . '/', // con trailing slash (encoded)
676 rtrim($pagelink_decoded, '/'), // senza trailing slash (decoded)
677 rtrim($pagelink_decoded, '/') . '/', // con trailing slash (decoded)
678 $original_lang,
679 $lang
680 ));
681
682 if ($result && !empty($result->translated_alias)) {
683 // Usa translated_alias dal DB
684 $hreflang_url = $result->translated_alias;
685 } else {
686 // Fallback: usa pagelink con slug lingua
687 $hreflang_url = $pagelink_with_lang;
688 }
689 }
690 } else {
691 // Lingue tradotte
692 // Costruisci pagelink con slug lingua per la query
693 $original_path_only = parse_url($original_url_no_lang, PHP_URL_PATH);
694
695 // Remove subfolder from path if needed
696 if (!empty($settings['subfolder_installation'])) {
697 $home_path = wp_parse_url(home_url(), PHP_URL_PATH) ?? '';
698 if ($home_path && strpos($original_path_only, $home_path) === 0) {
699 $original_path_only = substr($original_path_only, strlen($home_path));
700 }
701 }
702
703 $pagelink_with_lang = rtrim($site_url, '/') . '/' . $lang . rtrim($original_path_only, '/') . '/';
704
705 // Prepare decoded version for URL matching
706 $pagelink_decoded = urldecode($pagelink_with_lang);
707
708 // Query per trovare translated_alias con varianti (con/senza trailing slash + encoded/decoded)
709 $result = $wpdb->get_row($wpdb->prepare(
710 "SELECT translated_alias FROM {$table_name}
711 WHERE (pagelink = %s OR pagelink = %s OR pagelink = %s OR pagelink = %s)
712 AND languageoriginal = %s
713 AND languagetranslated = %s
714 AND published = 1
715 LIMIT 1",
716 rtrim($pagelink_with_lang, '/'), // senza trailing slash (encoded)
717 rtrim($pagelink_with_lang, '/') . '/', // con trailing slash (encoded)
718 rtrim($pagelink_decoded, '/'), // senza trailing slash (decoded)
719 rtrim($pagelink_decoded, '/') . '/', // con trailing slash (decoded)
720 $original_lang,
721 $lang
722 ));
723
724 if ($result && !empty($result->translated_alias)) {
725 // Usa translated_alias dal DB
726 $hreflang_url = $result->translated_alias;
727 } else {
728 // Fallback: usa pagelink con slug lingua
729 $hreflang_url = $pagelink_with_lang;
730 }
731 }
732
733 // Output hreflang
734 echo '<link rel="alternate" hreflang="' . esc_attr($lang) . '" href="' . esc_url($hreflang_url) . "\" />\n";
735
736 // If alternative flags are configured for this language, also emit the localized hreflang
737 // (e.g. es + es-MX, en + en-US) using the same translated URL.
738 $localized_hreflang = $get_localized_hreflang($lang);
739 if (!empty($localized_hreflang) && strcasecmp($localized_hreflang, $lang) !== 0) {
740 $localized_hreflang_key = strtolower($localized_hreflang);
741 if (!isset($emitted_localized_hreflangs[$localized_hreflang_key])) {
742 echo '<link rel="alternate" hreflang="' . esc_attr($localized_hreflang) . '" href="' . esc_url($hreflang_url) . "\" />\n";
743 $emitted_localized_hreflangs[$localized_hreflang_key] = true;
744 }
745 }
746
747 // x-default = lingua originale
748 if ($lang === $original_lang) {
749 echo '<link rel="alternate" hreflang="x-default" data-gpthreflang="1" href="' . esc_url($hreflang_url) . "\" />\n";
750 }
751 }
752 });
753
754 /**
755 * Handle the multilanguage URLs
756 */
757 if ($settings ['rewrite_language_url'] == 1 && !$polylangActive) {
758 // Prevent WP to redirect
759 add_filter ( 'redirect_canonical', function ($redirect_url, $requested_url) use ($gpt_supported_languages) {
760 if (defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
761 return false;
762 }
763 }, 10, 2 );
764
765 /**
766 * Add rewrite rules to allow /lang/ URLs
767 */
768 add_filter ( 'rewrite_rules_array', function ($rules) use ($gpt_routable_languages) {
769 // Only enabled languages get a rewrite rule - registering one for every language
770 // the plugin can theoretically translate to (the full $gpt_supported_languages list)
771 // is what let WordPress resolve /hu/, /ro/, /no/, etc. as real pages even when those
772 // languages were never enabled, serving 200 duplicate content instead of a 404.
773 $new_rules = [ ];
774 foreach ( $gpt_routable_languages as $lang ) {
775 $new_rules ["{$lang}/(.*)$"] = 'index.php?pagename=$matches[1]&gptlang=' . $lang;
776 $new_rules ["{$lang}$"] = 'index.php?gptlang=' . $lang;
777 }
778 return $new_rules + $rules;
779 } );
780
781 /**
782 * Redirect URLs without language prefix to default language
783 */
784 add_action ( 'template_redirect', function () use ($gpt_supported_languages, $settings) {
785 $defaultOriginalLanguage = get_option ( 'gptranslate_options' ) ['language'] ?? 'en';
786
787 $default_lang = $defaultOriginalLanguage;
788 $uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
789
790 // Exceptions and management for subfolder installations
791 if($settings['subfolder_installation']) {
792 // Explode URI into parts
793 $uri_parts = explode('/', ltrim($uri, '/'));
794
795 // Remove the first part (subfolder)
796 array_shift($uri_parts);
797
798 // Rebuild the URI
799 $uri = '/' . implode('/', $uri_parts);
800 }
801
802 // Avoid multiple redirects in the same session
803 if (! session_id ()) {
804 session_start ();
805 }
806
807 if (! empty ( $_SESSION ['gptranslate_redirected'] ) && ($_SESSION ['gptranslate_redirected'] === $uri || $settings ['subfolder_installation'])) {
808 unset ( $_SESSION ['gptranslate_redirected'] );
809 return;
810 }
811
812 // Skip static assets and admin/API routes
813 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) {
814 return;
815 }
816
817 // If a language has already been detected and defined, skip redirect
818 if (defined ( 'GPTRANSLATE_CURRENT_LANG' )) {
819 return;
820 }
821
822 // Skip the default language if enabled
823 if(isset($settings ['omit_prefix_original_language']) && $settings ['omit_prefix_original_language'] == 1) {
824 return;
825 }
826
827 // Redirect to default language
828 $_SESSION ['gptranslate_redirected'] = $uri;
829 session_write_close();
830 wp_redirect ( home_url ( "/{$default_lang}{$uri}" ), 301 );
831 exit ();
832 } );
833
834 /**
835 * Add gptlang as a recognized query var
836 */
837 add_filter ( 'query_vars', function ($vars) {
838 $vars [] = 'gptlang';
839 return $vars;
840 } );
841
842 /**
843 * Helper function to get current language
844 */
845 function gpt_get_current_lang() {
846 if (defined ( 'GPTRANSLATE_CURRENT_LANG' ))
847 return GPTRANSLATE_CURRENT_LANG;
848 $lang = get_query_var ( 'gptlang' );
849 return $lang ?: 'en';
850 }
851
852 /**
853 * Rewrite internal links to include language prefix
854 */
855 if ($settings ['rewrite_page_links'] == 1 && !$polylangActive) {
856 add_action('template_redirect', function () use ($gpt_supported_languages, $settings) {
857 ob_start(function ($html) use ($gpt_supported_languages, $settings) {
858 $current_lang = defined('GPTRANSLATE_CURRENT_LANG') ? GPTRANSLATE_CURRENT_LANG : ($settings['language'] ?? 'en');
859
860 $site_url = home_url();
861 $parsed_site_url = wp_parse_url($site_url);
862 $base_path = rtrim($parsed_site_url['path'] ?? '', '');
863 $exclusions = $settings['rewrite_page_links_exclusions'] ?? '';
864 $page_exclusions = $settings['page_exclusions'] ?? '';
865
866 return preg_replace_callback('#<a\s[^>]*href=["\']([^"\']+)["\']#i', function ($matches) use ($current_lang, $gpt_supported_languages, $parsed_site_url, $base_path, $site_url, $settings, $exclusions, $page_exclusions) {
867 $original_url = $matches[1];
868 $url = $original_url;
869
870 // Check if link should be excluded from rewriting
871 if (gptranslate_should_exclude_link($url, $exclusions, $page_exclusions)) {
872 return $matches[0];
873 }
874
875 // Skip image links (lightbox etc.)
876 $allowed_extensions = [
877 'jpg', 'jpeg', 'png', 'gif', 'webp', 'webm', 'svg', 'mp4', 'mp3',
878 'bmp', 'tiff', 'css', 'js', 'pdf', 'zip',
879 'rar', 'woff', 'woff2', 'ttf', 'eot', 'ico'
880 ];
881 $lower_url = strtolower(parse_url($url, PHP_URL_PATH) ?: '');
882 foreach ($allowed_extensions as $ext) {
883 if (str_ends_with($lower_url, '.' . $ext)) {
884 return $matches[0];
885 }
886 }
887
888 // Skip external URLs
889 if (preg_match('#^https?://#', $url) && strpos($url, $parsed_site_url['host']) === false) {
890 return $matches[0];
891 }
892
893 // Skip admin-related URLs
894 if (strpos($url, 'wp-admin') !== false || strpos($url, 'wp-login.php') !== false || strpos($url, 'wp-json') !== false) {
895 return $matches[0];
896 }
897
898 // Skip adding prefix for default language if enabled
899 $defaultOriginalLanguage = get_option ( 'gptranslate_options' ) ['language'] ?? 'en';
900 if (isset($settings['omit_prefix_original_language']) && $settings['omit_prefix_original_language'] == 1 && $current_lang == $defaultOriginalLanguage) {
901 return $matches[0];
902 }
903
904 // Convert absolute local URL to relative
905 if (strpos($url, $parsed_site_url['scheme'] . '://' . $parsed_site_url['host']) === 0) {
906 $url = substr($url, strlen($parsed_site_url['scheme'] . '://' . $parsed_site_url['host']));
907 if ($url === '' || $url === false) { $url = '/'; }
908 }
909
910 // Remove base path if present
911 if (!empty($base_path) && strpos($url, $base_path) === 0) {
912 $url = substr($url, strlen($base_path));
913 }
914
915 // Ensure it starts with /
916 if (strpos($url, '/') !== 0) {
917 return $matches[0]; // ignore non-relative, non-local URLs
918 }
919
920 // Verifica se lo slug della lingua � presente, e se s�, sostituiscilo con la lingua corrente
921 foreach ($gpt_supported_languages as $lang) {
922 if (preg_match("#^/{$lang}(/|\$)#i", $url)) {
923 // Se la lingua trovata � diversa dalla lingua corrente, sostituire lo slug della lingua
924 if ($lang !== $current_lang) {
925 // Sostituisci lo slug con la lingua corrente
926 $url = preg_replace("#^/{$lang}#", "/{$current_lang}", $url);
927 }
928 return str_replace($original_url, $url, $matches[0]);
929 }
930 }
931
932 // Build new full absolute URL
933 $new_url = rtrim($site_url, '/') . '/' . $current_lang . $url;
934
935 return str_replace($original_url, $new_url, $matches[0]);
936
937 }, $html);
938 });
939 }, 15);
940 }
941 }
942
943 /**
944 * Add canonical link tag to the head
945 */
946 add_action('wp_head', function() use ($settings) {
947
948 // Check if canonical is enabled
949 if (empty($settings['add_canonical'])) {
950 return;
951 }
952
953 // Only apply to HTML pages (not feeds, REST API, etc.)
954 if (is_feed() || is_robots() || is_trackback()) {
955 return;
956 }
957
958 // ------------------------------------------------------------------
959 // Get CURRENT PUBLIC URL (translated), not the rewritten WP one
960 // ------------------------------------------------------------------
961 $pageLink = '';
962
963 $publicRequestUri = $GLOBALS['gptranslate_public_request_uri'] ?? '';
964
965 if (!empty($publicRequestUri)) {
966
967 // Build absolute URL from home_url() origin + original REQUEST_URI
968 $homeParts = wp_parse_url(home_url());
969
970 $origin = ($homeParts['scheme'] ?? 'http') . '://' . ($homeParts['host'] ?? '');
971 if (!empty($homeParts['port'])) {
972 $origin .= ':' . $homeParts['port'];
973 }
974
975 $pageLink = $origin . $publicRequestUri;
976
977 // Canonical should not include query string or fragment
978 $parsed = wp_parse_url($pageLink);
979 if (!empty($parsed['scheme']) && !empty($parsed['host'])) {
980 $rebuilt = $parsed['scheme'] . '://' . $parsed['host'];
981 if (!empty($parsed['port'])) {
982 $rebuilt .= ':' . $parsed['port'];
983 }
984 $rebuilt .= $parsed['path'] ?? '/';
985 $pageLink = $rebuilt;
986 }
987
988 // WordPress-style trailing slash consistency
989 if (function_exists('gpt_trailingslashit_url')) {
990 $pageLink = gpt_trailingslashit_url($pageLink);
991 }
992
993 } else {
994 // Fallback: costruiamo l'URL con la lingua corrente
995 $current_lang = defined('GPTRANSLATE_CURRENT_LANG') ? GPTRANSLATE_CURRENT_LANG : '';
996 $default_lang = $settings['language'];
997
998 if ((empty($current_lang) || $current_lang === $default_lang) && $settings ['omit_prefix_original_language'] == 1) {
999 // Se siamo sulla lingua di default, URL normale
1000 $pageLink = home_url(add_query_arg(null, null));
1001 } else {
1002 // Aggiungiamo il prefisso lingua all'URL corrente
1003 global $wp;
1004 $current_path = $wp->request;
1005
1006 // Rimuoviamo eventuali prefissi di lingua gi� presenti
1007 $current_path = preg_replace('#^[a-z]{2}/#', '', $current_path);
1008
1009 $pageLink = home_url('/' . $current_lang . '/' . ltrim($current_path, '/'));
1010 }
1011 }
1012
1013 // Optional urldecode (user setting)
1014 if (!empty($settings['serverside_translations_urldecode'])) {
1015 $pageLink = urldecode($pageLink);
1016 }
1017
1018 // ------------------------------------------------------------------
1019 // Remove existing canonical tags (Yoast, Rank Math, Core, etc.)
1020 // ------------------------------------------------------------------
1021 remove_action('wp_head', 'rel_canonical');
1022 remove_action('wp_head', 'wp_shortlink_wp_head');
1023
1024 // ------------------------------------------------------------------
1025 // Remove canonical from Yoast SEO
1026 // ------------------------------------------------------------------
1027 if (defined('WPSEO_VERSION')) {
1028 remove_action('wpseo_head', ['WPSEO_Frontend', 'canonical'], 20);
1029 // Alternative methods for different Yoast versions
1030 if (class_exists('WPSEO_Frontend')) {
1031 $instance = WPSEO_Frontend::get_instance();
1032 remove_action('wp_head', [$instance, 'canonical'], 20);
1033 }
1034 }
1035
1036 // ------------------------------------------------------------------
1037 // Remove canonical from Rank Math SEO
1038 // ------------------------------------------------------------------
1039 if (class_exists('RankMath')) {
1040 remove_action('rank_math/head', 'rank_math_canonical');
1041 remove_action('rank_math/head', ['RankMath\Frontend\Head', 'canonical'], 20);
1042 if (function_exists('rank_math')) {
1043 remove_action('wp_head', [rank_math()->frontend, 'canonical'], 1);
1044 }
1045 }
1046
1047 // ------------------------------------------------------------------
1048 // Remove canonical from SureRank
1049 // ------------------------------------------------------------------
1050 if (class_exists('SureRank\Inc\Frontend\Canonical')) {
1051 remove_action('surerank_print_meta', [SureRank\Inc\Frontend\Canonical::get_instance(), 'print_canonical_url'], 1);
1052 }
1053
1054 // ------------------------------------------------------------------
1055 // Remove canonical from All in One SEO (AIOSEO)
1056 // ------------------------------------------------------------------
1057 if (function_exists('aioseo')) {
1058 remove_action('wp_head', 'aioseo_canonical_tag', 1);
1059 if (class_exists('AIOSEO\Plugin\Common\Models\Post')) {
1060 add_filter('aioseo_canonical_url', '__return_false');
1061 }
1062 }
1063
1064 // ------------------------------------------------------------------
1065 // Remove canonical from SEOPress
1066 // ------------------------------------------------------------------
1067 if (function_exists('seopress_init')) {
1068 remove_action('wp_head', 'seopress_canonical', 1);
1069 add_filter('seopress_titles_canonical', '__return_false');
1070
1071 // ------------------------------------------------------------------
1072 // Translate SEOPress title and description for translated pages
1073 // ------------------------------------------------------------------
1074 add_filter('seopress_titles_title', function($title) use ($settings) {
1075 if (!defined('GPTRANSLATE_CURRENT_LANG')) {
1076 return $title;
1077 }
1078
1079 $current_lang = GPTRANSLATE_CURRENT_LANG;
1080 $default_lang = $settings['language'];
1081
1082 // Only translate if current language is different from default
1083 if ($current_lang === $default_lang) {
1084 return $title;
1085 }
1086
1087 global $wpdb;
1088
1089 // Build the current page URL for translation lookup
1090 $publicRequestUri = $GLOBALS['gptranslate_public_request_uri'] ?? '';
1091
1092 if (!empty($publicRequestUri)) {
1093 $homeParts = wp_parse_url(home_url());
1094 $origin = ($homeParts['scheme'] ?? 'http') . '://' . ($homeParts['host'] ?? '');
1095 if (!empty($homeParts['port'])) {
1096 $origin .= ':' . $homeParts['port'];
1097 }
1098 $pageLink = $origin . $publicRequestUri;
1099 } else {
1100 global $wp;
1101 $current_path = $wp->request ?? '';
1102 $current_path = preg_replace('#^[a-z]{2}/#', '', $current_path);
1103 $pageLink = home_url('/' . $current_lang . '/' . ltrim($current_path, '/'));
1104 }
1105
1106 // Look up translations from GPTranslate DB
1107 $row = $wpdb->get_row($wpdb->prepare(
1108 "SELECT translations, alt_translations FROM {$wpdb->prefix}gptranslate WHERE pagelink = %s AND languagetranslated = %s AND published = 1 LIMIT 1",
1109 rtrim($pageLink, '/'),
1110 $current_lang
1111 ), ARRAY_A);
1112
1113 if (!$row) {
1114 return $title;
1115 }
1116
1117 // Merge translations and alt_translations
1118 $translations = json_decode($row['translations'], true) ?? [];
1119 $alt_translations = json_decode($row['alt_translations'], true) ?? [];
1120 $all_translations = array_merge($translations, $alt_translations);
1121
1122 // Sort by key length (longest first) for better matching
1123 uksort($all_translations, fn($a, $b) => strlen($b) - strlen($a));
1124
1125 // Apply translations to title (case-insensitive, partial match)
1126 $translated_title = $title;
1127 $case_insensitive = !empty($settings['serverside_translations_caseinsensitive']);
1128
1129 foreach ($all_translations as $original => $translated) {
1130 $prev = $translated_title;
1131 if ($case_insensitive) {
1132 $translated_title = str_ireplace(trim($original), $translated, $translated_title);
1133 } else {
1134 $translated_title = str_replace(trim($original), $translated, $translated_title);
1135 }
1136 // Stop after first match (same logic as server-side translation)
1137 if ($translated_title !== $prev) {
1138 break;
1139 }
1140 }
1141
1142 return $translated_title;
1143 });
1144
1145 // Translate SEOPress meta description
1146 add_filter('seopress_titles_desc', function($description) use ($settings) {
1147 if (!defined('GPTRANSLATE_CURRENT_LANG')) {
1148 return $description;
1149 }
1150
1151 $current_lang = GPTRANSLATE_CURRENT_LANG;
1152 $default_lang = $settings['language'];
1153
1154 // Only translate if current language is different from default
1155 if ($current_lang === $default_lang) {
1156 return $description;
1157 }
1158
1159 global $wpdb;
1160
1161 // Build the current page URL for translation lookup
1162 $publicRequestUri = $GLOBALS['gptranslate_public_request_uri'] ?? '';
1163
1164 if (!empty($publicRequestUri)) {
1165 $homeParts = wp_parse_url(home_url());
1166 $origin = ($homeParts['scheme'] ?? 'http') . '://' . ($homeParts['host'] ?? '');
1167 if (!empty($homeParts['port'])) {
1168 $origin .= ':' . $homeParts['port'];
1169 }
1170 $pageLink = $origin . $publicRequestUri;
1171 } else {
1172 global $wp;
1173 $current_path = $wp->request ?? '';
1174 $current_path = preg_replace('#^[a-z]{2}/#', '', $current_path);
1175 $pageLink = home_url('/' . $current_lang . '/' . ltrim($current_path, '/'));
1176 }
1177
1178 // Look up translations from GPTranslate DB
1179 $row = $wpdb->get_row($wpdb->prepare(
1180 "SELECT translations, alt_translations FROM {$wpdb->prefix}gptranslate WHERE pagelink = %s AND languagetranslated = %s AND published = 1 LIMIT 1",
1181 rtrim($pageLink, '/'),
1182 $current_lang
1183 ), ARRAY_A);
1184
1185 if (!$row) {
1186 return $description;
1187 }
1188
1189 // Merge translations and alt_translations
1190 $translations = json_decode($row['translations'], true) ?? [];
1191 $alt_translations = json_decode($row['alt_translations'], true) ?? [];
1192 $all_translations = array_merge($translations, $alt_translations);
1193
1194 // Sort by key length (longest first) for better matching
1195 uksort($all_translations, fn($a, $b) => strlen($b) - strlen($a));
1196
1197 // Apply translations to description (case-insensitive, partial match)
1198 $translated_desc = $description;
1199 $case_insensitive = !empty($settings['serverside_translations_caseinsensitive']);
1200
1201 foreach ($all_translations as $original => $translated) {
1202 $prev = $translated_desc;
1203 if ($case_insensitive) {
1204 $translated_desc = str_ireplace(trim($original), $translated, $translated_desc);
1205 } else {
1206 $translated_desc = str_replace(trim($original), $translated, $translated_desc);
1207 }
1208 // Stop after first match
1209 if ($translated_desc !== $prev) {
1210 break;
1211 }
1212 }
1213
1214 return $translated_desc;
1215 });
1216 }
1217
1218 // ------------------------------------------------------------------
1219 // Remove canonical from The SEO Framework
1220 // ------------------------------------------------------------------
1221 if (function_exists('the_seo_framework')) {
1222 remove_action('wp_head', [the_seo_framework(), 'canonical_url'], 1);
1223 add_filter('the_seo_framework_rel_canonical_output', '__return_false');
1224 }
1225
1226 // ------------------------------------------------------------------
1227 // Remove canonical from Slim SEO
1228 // ------------------------------------------------------------------
1229 if (class_exists('SlimSEO\MetaTags\Canonical')) {
1230 add_filter('slim_seo_canonical_url', '__return_false');
1231 }
1232
1233 // ------------------------------------------------------------------
1234 // Remove canonical from Jetpack
1235 // ------------------------------------------------------------------
1236 if (class_exists('Jetpack')) {
1237 add_filter('jetpack_enable_canonical', '__return_false');
1238 }
1239
1240 // ------------------------------------------------------------------
1241 // Remove canonical from WP Meta SEO
1242 // ------------------------------------------------------------------
1243 if (class_exists('WpMetaSeo')) {
1244 add_filter('wpms_canonical_url', '__return_false');
1245 }
1246
1247 // ------------------------------------------------------------------
1248 // Generic filter to override any other canonical
1249 // ------------------------------------------------------------------
1250 add_filter('get_canonical_url', '__return_false', 999);
1251 add_filter('wpseo_canonical', '__return_false', 999);
1252 add_filter('aioseop_canonical_url', '__return_false', 999);
1253
1254 // Store canonical for output buffering (add this line before the echo)
1255 $GLOBALS['gptranslate_canonical_url'] = $pageLink;
1256 }, 1);
1257
1258 /**
1259 * Final safety layer: use output buffering to remove ALL canonical tags and add only GPTranslate's
1260 * This runs early in template_redirect to capture all HTML output
1261 * Same robust approach used for og:locale replacement
1262 */
1263 add_action('template_redirect', function() use ($settings) {
1264
1265 if (empty($settings['add_canonical'])) {
1266 return;
1267 }
1268
1269 // Skip admin pages
1270 if (is_admin()) {
1271 return;
1272 }
1273
1274 // First layer: attempt to block canonicals via filters (defense in depth)
1275 // Core
1276 remove_action('wp_head', 'rel_canonical');
1277
1278 // Yoast SEO
1279 add_filter('wpseo_canonical', '__return_false');
1280
1281 // Rank Math
1282 add_filter('rank_math/frontend/canonical', '__return_false');
1283
1284 // All in One SEO
1285 add_filter('aioseop_canonical_url', '__return_false');
1286
1287 // Second layer: output buffering to ensure clean canonical output
1288 ob_start(function($html) {
1289
1290 // Only proceed if we have a GPTranslate canonical URL
1291 if (empty($GLOBALS['gptranslate_canonical_url'])) {
1292 return $html;
1293 }
1294
1295 $gptCanonical = $GLOBALS['gptranslate_canonical_url'];
1296
1297 // Step 1: Remove ALL existing canonical tags (from any plugin or WordPress core)
1298 // This ensures no duplicate or conflicting canonicals
1299 $html = preg_replace(
1300 '/<link\s+rel=["\']canonical["\']\s+href=["\'][^"\']*["\']\s*\/?>/i',
1301 '',
1302 $html
1303 );
1304
1305 // Step 2: Add GPTranslate canonical after charset/viewport meta tags for optimal SEO placement
1306 // Fallback: if no meta tags found, place before closing </head>
1307 $gptCanonicalTag = '<link rel="canonical" href="' . esc_url($gptCanonical) . '" />' . "\n";
1308
1309 // Try to insert after viewport meta tag (most common modern pattern)
1310 $html = preg_replace(
1311 '/(<meta\s+name=["\']viewport["\']\s+content=["\'][^"\']*["\']\s*\/?>)/i',
1312 '$1' . "\n" . $gptCanonicalTag,
1313 $html,
1314 1,
1315 $count
1316 );
1317
1318 // If no viewport found, try after charset meta tag
1319 if ($count === 0) {
1320 $html = preg_replace(
1321 '/(<meta\s+charset=["\'][^"\']*["\']\s*\/?>)/i',
1322 '$1' . "\n" . $gptCanonicalTag,
1323 $html,
1324 1,
1325 $count
1326 );
1327 }
1328
1329 // Fallback: if no meta tags found, place before closing </head> tag
1330 if ($count === 0) {
1331 $html = preg_replace(
1332 '/(<\/head>)/i',
1333 $gptCanonicalTag . '$1',
1334 $html,
1335 1
1336 );
1337 }
1338
1339 return $html;
1340 });
1341
1342 }, 1);
1343
1344 /**
1345 * Force replace og:locale in final HTML output via output buffering
1346 * This ensures og:locale is correctly replaced even when SEO plugins override it
1347 */
1348 add_action('template_redirect', function() use ($settings, $polylangActive) {
1349 // Skip if GPTranslate current language is not defined
1350 if (!defined('GPTRANSLATE_CURRENT_LANG')) {
1351 return;
1352 }
1353
1354 // Skip if conditions are not met
1355 if ((!$settings['set_html_lang'] && !$settings['rewrite_language_url']) || $polylangActive) {
1356 return;
1357 }
1358
1359 // Skip admin pages
1360 if (is_admin()) {
1361 return;
1362 }
1363
1364 // Start output buffering to capture and modify HTML
1365 ob_start(function($html) use ($settings) {
1366 // Double-check GPTRANSLATE_CURRENT_LANG is still defined
1367 if (!defined('GPTRANSLATE_CURRENT_LANG')) {
1368 return $html;
1369 }
1370
1371 $altflags_array = get_option('gptranslate_options')['alt_flags'] ?? [];
1372
1373 // Same language mapping as language_attributes filter
1374 $lang_map = [
1375 'en' => (in_array('usa', $altflags_array) || in_array('canada', $altflags_array)) ? 'en-US' : 'en-GB',
1376 'fr' => in_array('quebec', $altflags_array) ? 'fr-CA' : 'fr-FR',
1377 'pt' => in_array('brazil', $altflags_array) ? 'pt-BR' : 'pt-PT',
1378 'es' => in_array('mexico', $altflags_array) ? 'es-MX' : (in_array('argentina', $altflags_array) ? 'es-AR' : (in_array('colombia', $altflags_array) ? 'es-CO' : 'es-ES')),
1379 'af' => 'af-ZA', 'sq' => 'sq-AL', 'am' => 'am-ET', 'ar' => 'ar-SA', 'hy' => 'hy-AM',
1380 'az' => 'az-AZ', 'eu' => 'eu-ES', 'be' => 'be-BY', 'bn' => 'bn-BD', 'bs' => 'bs-BA',
1381 'bg' => 'bg-BG', 'ca' => 'ca-ES', 'ceb' => 'ceb-PH', 'ny' => 'ny-MW', 'zh' => 'zh-CN',
1382 'zt' => 'zh-TW', 'co' => 'co-FR', 'hr' => 'hr-HR', 'cs' => 'cs-CZ', 'da' => 'da-DK',
1383 'nl' => 'nl-NL', 'eo' => 'eo-EO', 'et' => 'et-EE', 'tl' => 'tl-PH', 'fi' => 'fi-FI',
1384 'fy' => 'fy-NL', 'gl' => 'gl-ES', 'ka' => 'ka-GE', 'de' => 'de-DE', 'el' => 'el-GR',
1385 'gu' => 'gu-IN', 'ht' => 'ht-HT', 'ha' => 'ha-NG', 'haw' => 'haw-US', 'iw' => 'iw-IL',
1386 'hi' => 'hi-IN', 'hmn' => 'hmn-US', 'hu' => 'hu-HU', 'is' => 'is-IS', 'ig' => 'ig-NG',
1387 'id' => 'id-ID', 'ga' => 'ga-IE', 'it' => 'it-IT', 'ja' => 'ja-JP', 'jw' => 'jw-ID',
1388 'kn' => 'kn-IN', 'kk' => 'kk-KZ', 'km' => 'km-KH', 'ko' => 'ko-KR', 'ku' => 'ku-TR',
1389 'ky' => 'ky-KG', 'lo' => 'lo-LA', 'la' => 'la-VA', 'lv' => 'lv-LV', 'lt' => 'lt-LT',
1390 'lb' => 'lb-LU', 'mk' => 'mk-MK', 'mg' => 'mg-MG', 'ms' => 'ms-MY', 'ml' => 'ml-IN',
1391 'mt' => 'mt-MT', 'mi' => 'mi-NZ', 'mr' => 'mr-IN', 'mn' => 'mn-MN', 'my' => 'my-MM',
1392 'ne' => 'ne-NP', 'no' => 'no-NO', 'ps' => 'ps-AF', 'fa' => 'fa-IR', 'pl' => 'pl-PL',
1393 'pa' => 'pa-IN', 'ro' => 'ro-RO', 'ru' => 'ru-RU', 'sm' => 'sm-WS', 'gd' => 'gd-GB',
1394 'sr' => 'sr-RS', 'st' => 'st-LS', 'sn' => 'sn-ZW', 'sd' => 'sd-PK', 'si' => 'si-LK',
1395 'sk' => 'sk-SK', 'sl' => 'sl-SI', 'so' => 'so-SO', 'su' => 'su-ID', 'sw' => 'sw-TZ',
1396 'sv' => 'sv-SE', 'tg' => 'tg-TJ', 'ta' => 'ta-IN', 'te' => 'te-IN', 'th' => 'th-TH',
1397 'tr' => 'tr-TR', 'uk' => 'uk-UA', 'ur' => 'ur-PK', 'uz' => 'uz-UZ', 'vi' => 'vi-VN',
1398 'cy' => 'cy-GB', 'xh' => 'xh-ZA', 'yi' => 'yi-US', 'yo' => 'yo-NG', 'zu' => 'zu-ZA'
1399 ];
1400
1401 $gpt_lang = strtolower(GPTRANSLATE_CURRENT_LANG);
1402 $full_lang = $lang_map[$gpt_lang] ?? $gpt_lang;
1403
1404 // Convert format from xx-YY to xx_YY for og:locale (underscore instead of dash)
1405 $og_locale = str_replace('-', '_', $full_lang);
1406
1407 // Replace og:locale in HTML (handles various formats)
1408 $html = preg_replace(
1409 '/<meta\s+property=["\']og:locale["\']\s+content=["\'][^"\']*["\']\s*\/?>/i',
1410 '<meta property="og:locale" content="' . esc_attr($og_locale) . '" />',
1411 $html
1412 );
1413
1414 // Replace dc.language (Dublin Core) with the same locale format as og:locale
1415 $html = preg_replace(
1416 '/<meta\s+name=["\']dc\.language["\']\s+content=["\'][^"\']*["\']\s*\/?>/i',
1417 '<meta name="dc.language" content="' . esc_attr($og_locale) . '" />',
1418 $html
1419 );
1420
1421 // Replace og:url with the translated canonical URL (use the same URL as canonical)
1422 if (!empty($GLOBALS['gptranslate_canonical_url'])) {
1423 $translatedUrl = $GLOBALS['gptranslate_canonical_url'];
1424 $html = preg_replace(
1425 '/<meta\s+property=["\']og:url["\']\s+content=["\'][^"\']*["\']\s*\/?>/i',
1426 '<meta property="og:url" content="' . esc_attr($translatedUrl) . '" />',
1427 $html
1428 );
1429 }
1430
1431 return $html;
1432 });
1433 }, 1); // Priority 1 to run early
1434 } // end is_admin check
1435