multilang-routing.php
| 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 |