Changeset 3456920
- Timestamp:
- 02/09/2026 10:07:53 AM (12 days ago)
- Location:
- ai-translate/trunk
- Files:
-
- 7 edited
-
README.md (modified) (3 diffs)
-
ai-translate.php (modified) (19 diffs)
-
includes/admin-page.php (modified) (6 diffs)
-
includes/class-ai-cache-meta.php (modified) (4 diffs)
-
includes/class-ai-cache.php (modified) (1 diff)
-
includes/class-ai-ob.php (modified) (4 diffs)
-
includes/class-ai-seo.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
ai-translate/trunk/README.md
r3455458 r3456920 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 2.2. 56 Stable tag: 2.2.6 7 7 Requires PHP: 8.0 8 8 License: GPLv2 or later … … 185 185 ## Changelog 186 186 187 ### 2.2. 5187 ### 2.2.6 188 188 - Improve system prompt (role, what and how). 189 189 - Improve website context generator (what is the website about). … … 191 191 - All enabled and detectable languages added to Wordpress sitemap (/wp-sitemap.xml) 192 192 - Code base refactoring. Improve performance and security, thighening code. 193 193 194 194 195 ### 2.2.4 -
ai-translate/trunk/ai-translate.php
r3455458 r3456920 6 6 * Author: NetCare 7 7 * Author URI: https://netcare.nl/ 8 * Version: 2.2. 58 * Version: 2.2.6 9 9 * Requires at least: 5.0 10 10 * Tested up to: 6.9 … … 729 729 } 730 730 731 // RULE 0: Strip explicit ?lang= query parameter from URL → redirect to clean path-based URL 732 // The ?lang= parameter is only used internally by rewrite rules, never in actual visitor URLs. 733 // Examples: /es?lang=fr → /es/ | /?lang=es → /es/ | /?lang={default} → / 734 if (isset($_GET['lang']) && trim((string) $_GET['lang']) !== '') { 735 $cleanParams = $_GET; 736 unset($cleanParams['lang']); 737 738 $targetPath = $reqPath; 739 if ($langFromUrl === null) { 740 // No language in path yet — use the GET parameter to build the correct path 741 $getLang = strtolower(sanitize_key((string) $_GET['lang'])); 742 if ($getLang !== '' && $defaultLang && strtolower($getLang) !== strtolower((string) $defaultLang)) { 743 $targetPath = '/' . $getLang . '/'; 744 } 745 } 746 747 // Ensure trailing slash (except bare root) 748 if ($targetPath !== '/' && substr($targetPath, -1) !== '/') { 749 $targetPath .= '/'; 750 } 751 752 $targetUrl = home_url($targetPath); 753 if (!empty($cleanParams)) { 754 $targetUrl = add_query_arg($cleanParams, $targetUrl); 755 } 756 757 nocache_headers(); 758 wp_safe_redirect(esc_url_raw($targetUrl), 301); 759 exit; 760 } 761 731 762 $cookieLang = isset($_COOKIE['ai_translate_lang']) ? strtolower(sanitize_key((string) $_COOKIE['ai_translate_lang'])) : ''; 732 763 … … 767 798 768 799 // RULE 4: If cookie exists and on root, ensure language is set to default 769 // Skip this rule if there's a search parameter or explicit lang parameter (handled above) 770 $hasExplicitLang = isset($_GET['lang']) && trim((string) $_GET['lang']) !== ''; 771 if ($reqPath === '/' && $cookieLang !== '' && !$hasSearchParam && !$hasExplicitLang) { 800 // Skip this rule if there's a search parameter (explicit ?lang= is already handled by RULE 0) 801 if ($reqPath === '/' && $cookieLang !== '' && !$hasSearchParam) { 772 802 \AITranslate\AI_Lang::set_cookie((string) $defaultLang); 773 803 // Set language and stop processing … … 1098 1128 function cS(r){function n(t){return t?t.trim().replace(/\s+/g," "):""}var s=new Set();var ns=r.querySelectorAll?r.querySelectorAll("input,textarea,select,button,[title],[aria-label],img[alt],.initial-greeting,.chatbot-bot-text"):[];ns.forEach(function(el){if(el.closest?el.closest("[data-ai-trans-skip]"):el.hasAttribute("data-ai-trans-skip"))return;var ph=n(el.getAttribute("placeholder"));if(ph)s.add(ph);var tl=n(el.getAttribute("title"));if(tl)s.add(tl);var al=n(el.getAttribute("aria-label"));if(al)s.add(al);var at=n(el.getAttribute("alt"));if(at)s.add(at);var tg=(el.tagName||"").toLowerCase();if(tg==="input"){var tp=(el.getAttribute("type")||"").toLowerCase();if(tp==="submit"||tp==="button"||tp==="reset"){var v=n(el.getAttribute("value"));if(v)s.add(v);}}var tc=el.textContent;if((el.classList.contains("initial-greeting")||el.classList.contains("chatbot-bot-text"))&&tc){var tcn=n(tc);if(tcn)s.add(tcn);}});return Array.from(s);} 1099 1129 function aT(r,m){var ns=r.querySelectorAll?r.querySelectorAll("input,textarea,select,button,[title],[aria-label],img[alt],.initial-greeting,.chatbot-bot-text"):[];ns.forEach(function(el){if(el.closest?el.closest("[data-ai-trans-skip]"):el.hasAttribute("data-ai-trans-skip"))return;var ph=el.getAttribute("placeholder");if(ph){var pht=ph.trim();if(pht&&m[pht]!=null)el.setAttribute("placeholder",m[pht]);}var tl=el.getAttribute("title");if(tl){var tlt=tl.trim();if(tlt&&m[tlt]!=null)el.setAttribute("title",m[tlt]);}var al=el.getAttribute("aria-label");if(al){var alt=al.trim();if(alt&&m[alt]!=null)el.setAttribute("aria-label",m[alt]);}var at=el.getAttribute("alt");if(at){var att=at.trim();if(att&&m[att]!=null)el.setAttribute("alt",m[att]);}var tg=(el.tagName||"").toLowerCase();if(tg==="input"){var tp=(el.getAttribute("type")||"").toLowerCase();if(tp==="submit"||tp==="button"||tp==="reset"){var v=el.getAttribute("value");if(v){var vt=v.trim();if(vt&&m[vt]!=null)el.setAttribute("value",m[vt]);}}}var tc=el.textContent;if((el.classList.contains("initial-greeting")||el.classList.contains("chatbot-bot-text"))&&tc){var tct=tc.trim();if(tct&&m[tct]!=null)el.textContent=m[tct];}});} 1100 function tA(r){if(tA.called)return;tA.called=true;var ss=cS(r);if(!ss.length){tA.called=false;return;}var x=new XMLHttpRequest();x.open("POST",AI_TA.u,true);x.setRequestHeader("Content-Type","application/json; charset=UTF-8");x.onreadystatechange=function(){if(x.readyState===4){tA.called=false;if(x.status===200){try{var resp=JSON.parse(x.responseText);if(resp&&resp.success&&resp.data&&resp.data.map){aT(r,resp.data.map);}}catch(e){}}}};x.send(JSON.stringify({nonce:AI_TA.n,lang:gL(),strings:ss}));}1130 function tA(r){if(tA.called)return;tA.called=true;var ua=(typeof navigator!=="undefined"&&navigator.userAgent)?navigator.userAgent:"";if(/googlebot|bingbot|yandexbot|baiduspider|duckduckbot|slurp|facebot|ia_archiver/i.test(ua)){tA.called=false;return;}var ss=cS(r);if(!ss.length){tA.called=false;return;}var x=new XMLHttpRequest();x.open("POST",AI_TA.u,true);x.setRequestHeader("Content-Type","application/json; charset=UTF-8");x.onreadystatechange=function(){if(x.readyState===4){tA.called=false;if(x.status===200){try{var resp=JSON.parse(x.responseText);if(resp&&resp.success&&resp.data&&resp.data.map){aT(r,resp.data.map);}}catch(e){}}}};x.send(JSON.stringify({nonce:AI_TA.n,lang:gL(),strings:ss}));} 1101 1131 document.addEventListener("DOMContentLoaded",function(){var checkPage=function(){if(document.readyState==="complete"){setTimeout(function(){tA(document);},3000);}else{setTimeout(checkPage,100);}};checkPage();var moT=null,sel="input,textarea,select,button,[title],[aria-label],img[alt],.initial-greeting,.chatbot-bot-text";if(typeof MutationObserver!=="undefined"&&gL()){var mo=new MutationObserver(function(muts){for(var f=false,i=0;i<muts.length&&!f;i++){var a=muts[i].addedNodes;for(var j=0;j<a.length&&!f;j++){var n=a[j];if(n.nodeType===1){if(n.matches&&n.matches(sel))f=true;else if(n.querySelector&&n.querySelector(sel))f=true;}}}if(f){clearTimeout(moT);moT=setTimeout(function(){tA(document);},500);}});mo.observe(document.body||document.documentElement,{childList:true,subtree:true});}}); 1102 1132 })();</script>'; … … 1173 1203 'methods' => 'POST', 1174 1204 'permission_callback' => function (\WP_REST_Request $request) { 1205 // Bot check first: bots get empty response anyway, no need for referer/nonce 1206 $ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : ''; 1207 $is_bot = $ua !== '' && preg_match('/googlebot|googleother|bingbot|yandexbot|baiduspider|duckduckbot|slurp|facebot|ia_archiver/i', $ua); 1208 if ($is_bot) { 1209 $request->set_param('_ai_tr_bot_no_nonce', 1); 1210 return true; 1211 } 1212 // Referer check: must originate from this site 1213 $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; 1214 if ($referer === '' || strpos($referer, home_url()) !== 0) { 1215 return new \WP_Error('rest_forbidden', 'Invalid referer', ['status' => 403]); 1216 } 1175 1217 $nonce = $request->get_header('X-WP-Nonce'); 1176 1218 if ($nonce && wp_verify_nonce($nonce, 'wp_rest')) { … … 1185 1227 'args' => [], 1186 1228 'callback' => function (\WP_REST_Request $request) { 1229 $is_bot_request = (bool) $request->get_param('_ai_tr_bot_no_nonce'); 1187 1230 // Rate limiting: max 60 requests per minute per IP. 1188 1231 // This endpoint is called by client-side JavaScript for UI attribute translation. … … 1192 1235 $rate_key = 'ai_tr_rate_' . md5($ip); 1193 1236 $rate_count = (int) get_transient($rate_key); 1194 if ($rate_count >= 60) {1237 if ($rate_count >= 30) { 1195 1238 return new \WP_REST_Response(['error' => 'Rate limit exceeded'], 429); 1196 1239 } … … 1201 1244 $arr = []; 1202 1245 } 1246 // Input limits: max 50 strings of max 150 chars each (UI attributes are short) 1247 $arr = array_slice($arr, 0, 50); 1248 $arr = array_filter($arr, function ($s) { 1249 return is_string($s) && mb_strlen($s) <= 150; 1250 }); 1203 1251 // Normalize texts: trim and collapse multiple whitespace to single space 1204 1252 // Keep mapping between original and normalized for response … … 1230 1278 // Fallback: try to detect from Referer header if available (more reliable than current() for REST calls) 1231 1279 $referer = isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : ''; 1280 // Normalize referrer: remove double slashes in path (e.g., /de// -> /de/) 1281 if ($referer !== '') { 1282 $refererParsed = parse_url($referer); 1283 if (isset($refererParsed['path'])) { 1284 $refererParsed['path'] = preg_replace('#/+#', '/', $refererParsed['path']); 1285 $referer = ''; 1286 if (isset($refererParsed['scheme'])) { 1287 $referer .= $refererParsed['scheme'] . '://'; 1288 } 1289 if (isset($refererParsed['host'])) { 1290 $referer .= $refererParsed['host']; 1291 if (isset($refererParsed['port'])) { 1292 $referer .= ':' . $refererParsed['port']; 1293 } 1294 } 1295 $referer .= $refererParsed['path']; 1296 if (isset($refererParsed['query'])) { 1297 $referer .= '?' . $refererParsed['query']; 1298 } 1299 if (isset($refererParsed['fragment'])) { 1300 $referer .= '#' . $refererParsed['fragment']; 1301 } 1302 } 1303 } 1232 1304 if ($referer !== '' && preg_match('#/([a-z]{2})(?:/|$)#i', parse_url($referer, PHP_URL_PATH) ?: '', $m)) { 1233 1305 $lang = strtolower($m[1]); … … 1258 1330 // Batch-strings are UI attributes that should not be translated if stop_translations is enabled 1259 1331 $stop_translations = isset($settings['stop_translations_except_cache_invalidation']) ? (bool) $settings['stop_translations_except_cache_invalidation'] : false; 1260 1261 if ($stop_translations) { 1262 } 1332 // Bots get cached translations but no API calls (same as stop_translations) 1333 $skip_api = $stop_translations || $is_bot_request; 1263 1334 1264 1335 $map = []; … … 1315 1386 $cacheInvalid = false; 1316 1387 1317 // If stop_translations is enabled , always use cache (don't validate)1318 if (!$s top_translations) {1388 // If stop_translations is enabled or bot request, always use cache (don't validate) 1389 if (!$skip_api) { 1319 1390 // Validate cache: check if translation is exactly identical to source 1320 1391 // NOTE: Identical translations are now accepted as valid (text was already in target language) … … 1342 1413 } 1343 1414 1344 if ($cacheInvalid && !$s top_translations) {1415 if ($cacheInvalid && !$skip_api) { 1345 1416 // Cache entry is invalid - delete it and re-translate 1346 1417 ai_translate_delete_attr_transient($attrCacheKey); … … 1357 1428 } else { 1358 1429 // Text not in cache 1359 if ($s top_translations) {1360 // Stop translations enabled: use source text without API call1430 if ($skip_api) { 1431 // Stop translations or bot: use source text without API call 1361 1432 $originalText = isset($textsOriginal[$normalized]) ? $textsOriginal[$normalized] : $normalized; 1362 1433 $map[$originalText] = $originalText; … … 1382 1453 } 1383 1454 if (!empty($toTranslate)) { 1384 if ($s top_translations) {1385 // Stop translations enabled: block API calls, use source texts1455 if ($skip_api) { 1456 // Stop translations or bot: block API calls, use source texts 1386 1457 // Use source texts for all segments that would have been translated 1387 1458 foreach ($toTranslate as $id => $origNormalized) { … … 1391 1462 // Clear $toTranslate to prevent API call 1392 1463 $toTranslate = []; 1464 } else { 1465 // Validate that strings actually exist on the page (security: prevent abuse) 1466 $referer = isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : ''; 1467 // Normalize referrer: remove double slashes in path (e.g., /de// -> /de/) 1468 if ($referer !== '') { 1469 $refererParsed = parse_url($referer); 1470 if (isset($refererParsed['path'])) { 1471 $refererParsed['path'] = preg_replace('#/+#', '/', $refererParsed['path']); 1472 $referer = ''; 1473 if (isset($refererParsed['scheme'])) { 1474 $referer .= $refererParsed['scheme'] . '://'; 1475 } 1476 if (isset($refererParsed['host'])) { 1477 $referer .= $refererParsed['host']; 1478 if (isset($refererParsed['port'])) { 1479 $referer .= ':' . $refererParsed['port']; 1480 } 1481 } 1482 $referer .= $refererParsed['path']; 1483 if (isset($refererParsed['query'])) { 1484 $referer .= '?' . $refererParsed['query']; 1485 } 1486 if (isset($refererParsed['fragment'])) { 1487 $referer .= '#' . $refererParsed['fragment']; 1488 } 1489 } 1490 } 1491 if ($referer !== '' && strpos($referer, home_url()) === 0) { 1492 // Get page HTML (cached using same expiration as translations) 1493 $page_cache_key = 'ai_tr_page_html_' . md5($referer); 1494 $page_html = get_transient($page_cache_key); 1495 if ($page_html === false) { 1496 // Fetch page HTML 1497 $response = wp_remote_get($referer, array( 1498 'timeout' => 5, 1499 'sslverify' => false, 1500 'headers' => array( 1501 'User-Agent' => 'AI-Translate-Validator/1.0' 1502 ) 1503 )); 1504 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { 1505 $page_html = wp_remote_retrieve_body($response); 1506 // Cache using same expiration as translations 1507 $expiry_hours = isset($settings['cache_expiration']) ? (int) $settings['cache_expiration'] : (14 * 24); 1508 $expiry = max(1, $expiry_hours) * HOUR_IN_SECONDS; 1509 set_transient($page_cache_key, $page_html, $expiry); 1510 } else { 1511 // Security: if page HTML cannot be fetched, block translation to prevent abuse 1512 $toTranslate = []; 1513 } 1514 } 1515 // Validate each string exists on page (in attributes or content) 1516 if ($page_html !== '') { 1517 $validated = []; 1518 foreach ($toTranslate as $id => $text) { 1519 // Check if string exists in HTML (escape special regex chars) 1520 $text_escaped = preg_quote($text, '/'); 1521 // Match in attributes (placeholder, title, aria-label, alt, value) or content 1522 if (preg_match('/' . $text_escaped . '/iu', $page_html)) { 1523 $validated[$id] = $text; 1524 } 1525 } 1526 // Only allow translation of validated strings 1527 $toTranslate = $validated; 1528 } else { 1529 // Security: if page HTML is empty, block translation to prevent abuse 1530 $toTranslate = []; 1531 } 1532 } else { 1533 // Security: if referer is invalid or missing, block translation 1534 $toTranslate = []; 1535 } 1393 1536 } 1394 1537 } … … 1639 1782 // Check if path contains a post type prefix (e.g., service/slug) 1640 1783 $slug_used_for_lookup = null; 1784 $detected_post_type = null; 1641 1785 if (strpos($rest, '/') !== false) { 1642 1786 $parts = explode('/', $rest, 2); 1643 1787 if (count($parts) === 2) { 1644 1788 $potential_post_type = $parts[0]; 1645 $potential_slug = $parts[1];1789 $potential_slug = trim($parts[1], '/'); 1646 1790 $slug_used_for_lookup = $potential_slug; 1647 1791 1648 1792 // Check if this is a registered post type 1649 1793 if (post_type_exists($potential_post_type)) { 1794 $detected_post_type = $potential_post_type; 1795 // If slug is empty (only post type, e.g., /it/service/), redirect to language root 1796 if ($potential_slug === '') { 1797 nocache_headers(); 1798 wp_redirect(home_url('/' . $lang . '/'), 301); 1799 exit; 1800 } 1650 1801 $post_id = \AITranslate\AI_Slugs::resolve_path_to_post($lang, $potential_slug); 1651 1802 if ($post_id) { … … 1659 1810 } 1660 1811 } 1812 } 1813 } else { 1814 // Single segment: check if it's a post type without slug (e.g., /it/service) 1815 if (post_type_exists($rest)) { 1816 $detected_post_type = $rest; 1817 // Post type without slug, redirect to language root 1818 nocache_headers(); 1819 wp_redirect(home_url('/' . $lang . '/'), 301); 1820 exit; 1661 1821 } 1662 1822 } … … 1824 1984 } 1825 1985 } 1986 1987 // No resolution found for this language-prefixed URL. 1988 // Remove the rewrite-generated pagename (e.g. "es/bestaat-niet") to prevent 1989 // WordPress from falling back to the blog index instead of properly triggering a 404. 1990 unset($wp->query_vars['pagename']); 1991 $wp->query_vars['error'] = '404'; 1826 1992 }); 1827 1993 … … 2248 2414 return; 2249 2415 } 2416 2417 // Rate limiting: max 10 search queries per minute per IP (prevent abuse/cost attacks) 2418 // Normal users rarely exceed 1-2 searches per minute; 5 is generous but prevents abuse 2419 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : 'unknown'; 2420 $rate_key = 'ai_tr_search_rate_' . md5($ip); 2421 $rate_count = (int) get_transient($rate_key); 2422 if ($rate_count >= 10) { 2423 // Rate limit exceeded: skip translation, use original query 2424 return; 2425 } 2426 set_transient($rate_key, $rate_count + 1, 60); 2250 2427 2251 2428 $settings = get_option('ai_translate_settings', array()); -
ai-translate/trunk/includes/admin-page.php
r3455458 r3456920 129 129 $post_id = intval($raw_post_id); 130 130 131 if ($post_id < 0) {131 if ($post_id < -1) { 132 132 wp_send_json_error(['message' => __('Invalid post ID.', 'ai-translate')]); 133 133 return; 134 134 } 135 135 136 // For homepage (post_id = 0) , skip post verification136 // For homepage (post_id = 0) and 404 page (post_id = -1), skip post verification 137 137 if ($post_id > 0) { 138 138 // Verify post exists and user has permission to access it … … 318 318 $front_page_id = (int) get_option('page_on_front'); 319 319 return ($front_page_id > 0) ? ('post:' . $front_page_id) : ('path:' . md5('/')); 320 } 321 if ($post_id === -1) { 322 return 'path:' . md5('/404'); 320 323 } 321 324 return 'post:' . $post_id; … … 416 419 } 417 420 418 // Check if response looks like an error page (40 4, 403, etc.)421 // Check if response looks like an error page (403, 500, etc.) 419 422 $is_error_page = false; 420 if ($http_code === 200 && !empty($response_body) ) {421 // Check for common error indicators in HTML 423 if ($http_code === 200 && !empty($response_body) && $post_id !== -1) { 424 // Check for common error indicators in HTML (skip for 404 pages which are expected) 422 425 if (stripos($response_body, '<title>404') !== false || 423 426 stripos($response_body, 'page not found') !== false || … … 428 431 } 429 432 430 // Accept 200 as success (unless it's an error page) 431 if ($http_code === 200 && !$is_error_page) { 433 // Accept 200 as success (unless it's an error page), and 404 for 404 page warming 434 $is_success = ($http_code === 200 && !$is_error_page) || ($post_id === -1 && $http_code === 404); 435 if ($is_success) { 432 436 // Wait a moment for async cache writes to complete 433 437 usleep(500000); // 0.5 second … … 658 662 $post_id = intval($raw_post_id); 659 663 660 if ($post_id < 0) {664 if ($post_id < -1) { 661 665 wp_send_json_error(['message' => __('Invalid post ID.', 'ai-translate')]); 662 666 return; 663 667 } 664 668 665 // For homepage (post_id = 0) , skip post verification669 // For homepage (post_id = 0) and 404 page (post_id = -1), skip post verification 666 670 if ($post_id > 0) { 667 671 // Verify post exists and user has permission to access it … … 692 696 } 693 697 694 // Get post URL (homepage uses home_url )698 // Get post URL (homepage uses home_url, 404 uses a non-existent path to trigger 404) 695 699 if ($post_id === 0) { 696 700 $post_url = home_url('/'); 701 } elseif ($post_id === -1) { 702 // 404 page: use a non-existent path that will trigger a 404 703 $post_url = home_url('/ait-404-cache-warm/'); 697 704 } else { 698 705 $post_url = get_permalink($post_id); -
ai-translate/trunk/includes/class-ai-cache-meta.php
r3455458 r3456920 77 77 $sql = "CREATE TABLE IF NOT EXISTS $table_name ( 78 78 id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 79 post_id BIGINT UNSIGNEDNOT NULL,79 post_id BIGINT NOT NULL, 80 80 language_code VARCHAR(10) NOT NULL, 81 81 cache_file VARCHAR(255) NOT NULL, … … 486 486 array_unshift($results, $homepage_obj); 487 487 } 488 489 // Add 404 page row (always show on first page, even if no cache yet - similar to homepage) 490 if ($offset === 0) { 491 $error404_cached = (int) $wpdb->get_var($wpdb->prepare( 492 "SELECT COUNT(DISTINCT language_code) FROM " . $table_name . " WHERE post_id = %d", 493 -1 494 )); 495 $error404_obj = new \stdClass(); 496 $error404_obj->ID = -1; 497 $error404_obj->post_type = '404'; 498 $error404_obj->post_title = __('404 Page', 'ai-translate'); 499 $error404_obj->cached_languages = $error404_cached; 500 $error404_obj->total_languages = $total_languages; 501 $error404_obj->percentage = $total_languages > 0 502 ? round(($error404_cached / $total_languages) * 100) 503 : 0; 504 $error404_obj->url = home_url('/404'); 505 $results[] = $error404_obj; 506 } 488 507 489 508 // Add percentage and URL … … 649 668 650 669 $post_id = (int) $row->post_id; 651 $title = $post_id === 0 ? __('Homepage', 'ai-translate') : get_the_title($post_id);670 $title = $post_id === 0 ? __('Homepage', 'ai-translate') : ($post_id === -1 ? __('404 Page', 'ai-translate') : get_the_title($post_id)); 652 671 if ($title === '') { 653 672 $title = sprintf(__('Post %d', 'ai-translate'), $post_id); … … 779 798 if ($post_id === 0) { 780 799 $path = '/'; 800 } elseif ($post_id === -1) { 801 // 404 page - no real permalink 802 return home_url('/' . $lang . '/404/'); 781 803 } else { 782 804 $permalink = get_permalink($post_id); -
ai-translate/trunk/includes/class-ai-cache.php
r3455458 r3456920 134 134 $post_id = 0; 135 135 } 136 // Check if route_id is the 404 page (path:md5(/404)) 137 elseif ($route_id === ('path:' . md5('/404'))) { 138 // Use post_id = -1 for 404 page 139 $post_id = -1; 140 } 136 141 137 142 if ($post_id !== null) { -
ai-translate/trunk/includes/class-ai-ob.php
r3455577 r3456920 679 679 } 680 680 681 // For 404 pages: use a single consistent route_id so all 404s share one cache entry per language 682 if (function_exists('is_404') && is_404()) { 683 return 'path:' . md5('/404'); 684 } 685 681 686 // CRITICAL: For warm cache requests and translated URLs, resolve the post ID from the URL FIRST 682 687 // This is needed because WordPress query functions (is_singular, get_queried_object_id) … … 878 883 private function should_skip_content_type($html) 879 884 { 880 // Skip 404 pages - multiple detection methods 885 // Skip 404 pages - but allow translation when a non-default language is active 886 // Users encountering a 404 in another language should see translated error messages 881 887 if ($this->is_404_page($html)) { 882 return true; 888 $lang = AI_Lang::current(); 889 if ($lang === null || !AI_Lang::should_translate($lang)) { 890 return true; 891 } 892 // Non-default language active: allow 404 through for translation 883 893 } 884 894 … … 907 917 private function should_skip_page_structure() 908 918 { 909 // Only translate singular posts/pages, with exceptions for search/homepage 919 // Only translate singular posts/pages, with exceptions for search/homepage/404 910 920 if (function_exists('is_singular') && !is_singular()) { 911 921 $is_search = function_exists('is_search') && is_search(); 912 922 $is_front_page = function_exists('is_front_page') && is_front_page(); 913 if (!$is_search && !$is_front_page) { 923 $is_404 = function_exists('is_404') && is_404(); 924 if (!$is_search && !$is_front_page && !$is_404) { 914 925 return true; 915 926 } … … 1222 1233 } 1223 1234 if (function_exists('is_front_page') && is_front_page()) { 1235 return true; 1236 } 1237 // Cache translated 404 pages - always same content, avoids repeated API calls 1238 if (function_exists('is_404') && is_404()) { 1224 1239 return true; 1225 1240 } -
ai-translate/trunk/includes/class-ai-seo.php
r3455458 r3456920 745 745 } 746 746 747 // 404 page: use a non-existent path to trigger a 404 response 748 if ((int) $post_id === -1) { 749 return home_url('/' . $lang . '/ait-404-cache-warm/'); 750 } 751 747 752 $translatedSlug = AI_Slugs::get_or_generate((int) $post_id, $lang); 748 753 if ($translatedSlug !== null && $translatedSlug !== '') {
Note: See TracChangeset
for help on using the changeset viewer.