Changeset 3461910
- Timestamp:
- 02/15/2026 04:00:51 PM (6 days ago)
- Location:
- ai-translate/trunk
- Files:
-
- 7 edited
-
README.md (modified) (3 diffs)
-
ai-translate.php (modified) (4 diffs)
-
includes/admin-page.php (modified) (1 diff)
-
includes/class-ai-ob.php (modified) (1 diff)
-
includes/class-ai-seo.php (modified) (7 diffs)
-
includes/class-ai-sitemap.php (modified) (5 diffs)
-
includes/class-ai-translate-core.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
ai-translate/trunk/README.md
r3456920 r3461910 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 2.2. 66 Stable tag: 2.2.7 7 7 Requires PHP: 8.0 8 8 License: GPLv2 or later … … 185 185 ## Changelog 186 186 187 ### 2.2. 6187 ### 2.2.7 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 - Better detect usable models from OpenAI. 194 - Change language switcher url to fixed urls instead of query param. 193 195 194 196 -
ai-translate/trunk/ai-translate.php
r3456920 r3461910 6 6 * Author: NetCare 7 7 * Author URI: https://netcare.nl/ 8 * Version: 2.2. 68 * Version: 2.2.7 9 9 * Requires at least: 5.0 10 10 * Tested up to: 6.9 … … 1004 1004 foreach ($enabled as $code) { 1005 1005 $code = sanitize_key($code); 1006 $label = strtoupper($code === $default ? $default : $code); 1007 // Use ?switch_lang= parameter to ensure cookie is set via init hook 1006 $isDefaultLang = (strtolower($code) === strtolower((string) $default)); 1007 $label = strtoupper($isDefaultLang ? $default : $code); 1008 // Use one canonical public URL format: 1009 // - default language => / 1010 // - non-default language => /{lang}/ 1008 1011 // Build relative URL to avoid host/home filters 1009 $url = '/?switch_lang=' . $code;1012 $url = $isDefaultLang ? '/' : '/' . $code . '/'; 1010 1013 $url = esc_url($url); 1011 1014 $flag = esc_url($flags_url . $code . '.png'); … … 1102 1105 foreach ($enabled as $code) { 1103 1106 $code = sanitize_key($code); 1107 $isDefaultLang = (strtolower($code) === strtolower((string) $default)); 1104 1108 1105 1109 // Skip current language … … 1108 1112 } 1109 1113 1110 $label = strtoupper($code === $default ? $default : $code); 1111 // Use ?switch_lang= parameter to ensure cookie is set via init hook 1114 $label = strtoupper($isDefaultLang ? $default : $code); 1115 // Use one canonical public URL format: 1116 // - default language => / 1117 // - non-default language => /{lang}/ 1112 1118 // Build relative URL to avoid host/home filters 1113 $url = '/?switch_lang=' . $code;1119 $url = $isDefaultLang ? '/' : '/' . $code . '/'; 1114 1120 $url = esc_url($url); 1115 1121 $flag = esc_url($flags_url . $code . '.png'); -
ai-translate/trunk/includes/admin-page.php
r3456920 r3461910 2528 2528 }, $data['data']); 2529 2529 $models = array_filter($models); 2530 // o1/o3 reasoning models don't support temperature; block from selection to avoid API errors 2530 // Filter model families known to be incompatible with text translation chat/completions. 2531 // Keep only models that are likely usable for this plugin's translation pipeline. 2531 2532 $models = array_filter($models, function ($model) { 2532 return !preg_match('/^(o1-|o3-)/i', $model); 2533 if (preg_match('/^(o1-|o3-)/i', $model)) { 2534 return false; 2535 } 2536 // Block non-chat capabilities even when they appear later in a GPT model ID. 2537 return !preg_match('/(dall-e|whisper|audio|image|realtime|transcribe|tts|embedding|moderation|codex|instruct)/i', $model); 2533 2538 }); 2534 2539 sort($models); -
ai-translate/trunk/includes/class-ai-ob.php
r3456920 r3461910 457 457 } 458 458 } 459 460 // Translate Twitter Card and JSON-LD text that Jetpack/plugins inject via wp_head. 461 // These run as regex on the final HTML because DOMDocument placeholder divs break 462 // the <head> structure, causing these meta/script tags to be inaccessible in the DOM pass. 463 $html3 = AI_SEO::translateTwitterCards($html3, $lang); 464 $html3 = AI_SEO::translateJsonLd($html3, $lang); 459 465 460 466 // Translate chatbot greeting strings embedded in inline script JSON (e.g. Kognetiks kchat_settings). -
ai-translate/trunk/includes/class-ai-seo.php
r3456920 r3461910 256 256 257 257 if ($ogLocale !== '') { 258 $existingOgLocale = $xpath->query('// head/meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:locale"]');258 $existingOgLocale = $xpath->query('//meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:locale"]'); 259 259 if ($existingOgLocale && $existingOgLocale->length > 0) { 260 260 for ($i = $existingOgLocale->length - 1; $i >= 0; $i--) { … … 377 377 } 378 378 if ($shouldReplaceOgDesc && $ogDesc !== '') { 379 $existingOgDesc = $xpath->query('// head/meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:description"]');379 $existingOgDesc = $xpath->query('//meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:description"]'); 380 380 if ($existingOgDesc && $existingOgDesc->length > 0) { 381 381 $ogDescElem = $existingOgDesc->item(0); … … 391 391 } 392 392 } elseif ($isTranslatedLang && $shouldReplaceOgDesc && $ogDesc === '') { 393 $existingOgDesc = $xpath->query('// head/meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:description"]');393 $existingOgDesc = $xpath->query('//meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:description"]'); 394 394 if ($existingOgDesc && $existingOgDesc->length > 0) { 395 395 $ogDescElem = $existingOgDesc->item(0); … … 407 407 } 408 408 if (($ogImageMissing || $shouldReplaceOgImage) && $ogImage !== '') { 409 $existingOgImage = $xpath->query('// head/meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:image"]');409 $existingOgImage = $xpath->query('//meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:image"]'); 410 410 if ($existingOgImage && $existingOgImage->length > 0) { 411 411 $ogImageElem = $existingOgImage->item(0); … … 422 422 } 423 423 if ($isTranslatedLang) { 424 $existingOgImageAlt = $xpath->query('// head/meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:image:alt"]');424 $existingOgImageAlt = $xpath->query('//meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="og:image:alt"]'); 425 425 if ($existingOgImageAlt && $existingOgImageAlt->length > 0) { 426 426 $ogImageAltElem = $existingOgImageAlt->item(0); … … 778 778 private static function isOgMissing($xpath, $prop) 779 779 { 780 $q = sprintf('//head/meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="%s"]', strtolower($prop)); 780 // Search entire document, not just <head>. DOMDocument may move OG tags from 781 // Jetpack/Yoast/RankMath to <body> when placeholder <div>s break <head> structure. 782 $q = sprintf('//meta[translate(@property, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="%s"]', strtolower($prop)); 781 783 $nodes = $xpath->query($q); 782 784 return !($nodes && $nodes->length > 0); … … 843 845 return home_url($path); 844 846 } 847 848 /** 849 * Translate Twitter Card meta tags in final HTML. 850 * Runs on the fully assembled HTML (after DOMDocument + placeholder restore) 851 * because DOMDocument may move Jetpack meta tags out of <head> when 852 * preceding script/style placeholders break the head structure. 853 * 854 * @param string $html Full HTML output. 855 * @param string $lang Target language code. 856 * @return string HTML with translated Twitter Card meta tags. 857 */ 858 public static function translateTwitterCards($html, $lang) 859 { 860 $default = AI_Lang::default(); 861 if ($default === null || strtolower((string) $lang) === strtolower((string) $default)) { 862 return $html; 863 } 864 865 $twitterNames = ['twitter:text:title', 'twitter:title', 'twitter:description', 'twitter:image:alt']; 866 $names = implode('|', array_map(function ($n) { return preg_quote($n, '#'); }, $twitterNames)); 867 $pattern = '#(<meta\s+name="(?:' . $names . ')"\s+content=")([^"]+)("[^>]*>)#i'; 868 869 return preg_replace_callback($pattern, function ($m) use ($default, $lang) { 870 $original = html_entity_decode($m[2], ENT_QUOTES, 'UTF-8'); 871 if (trim($original) === '') { 872 return $m[0]; 873 } 874 $translated = self::maybeTranslateMeta($original, $default, $lang); 875 if (!is_string($translated) || $translated === '' || $translated === $original) { 876 return $m[0]; 877 } 878 return $m[1] . esc_attr($translated) . $m[3]; 879 }, $html); 880 } 881 882 /** 883 * Translate JSON-LD BreadcrumbList name fields in final HTML. 884 * Called after script placeholders have been restored. 885 * 886 * @param string $html Full HTML output. 887 * @param string $lang Target language code. 888 * @return string HTML with translated JSON-LD. 889 */ 890 public static function translateJsonLd($html, $lang) 891 { 892 $default = AI_Lang::default(); 893 if ($default === null || strtolower((string) $lang) === strtolower((string) $default)) { 894 return $html; 895 } 896 897 return preg_replace_callback( 898 '#(<script\b[^>]*type=["\']application/ld\+json["\'][^>]*>)(.*?)(</script>)#is', 899 function ($m) use ($default, $lang) { 900 $json = json_decode($m[2], true); 901 if (!is_array($json)) { 902 return $m[0]; 903 } 904 905 $changed = self::translateJsonLdNode($json, $default, $lang); 906 if (!$changed) { 907 return $m[0]; 908 } 909 910 $encoded = wp_json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 911 if (!is_string($encoded) || $encoded === '') { 912 return $m[0]; 913 } 914 return $m[1] . $encoded . $m[3]; 915 }, 916 $html 917 ); 918 } 919 920 /** 921 * Recursively translate name/description fields in JSON-LD BreadcrumbList nodes. 922 * 923 * @param array &$node JSON-LD node (modified in place). 924 * @param string $default Default language. 925 * @param string $lang Target language. 926 * @return bool Whether any field was translated. 927 */ 928 private static function translateJsonLdNode(array &$node, $default, $lang) 929 { 930 $type = $node['@type'] ?? ''; 931 $changed = false; 932 933 if ($type === 'BreadcrumbList' && isset($node['itemListElement']) && is_array($node['itemListElement'])) { 934 foreach ($node['itemListElement'] as &$item) { 935 if (isset($item['name']) && is_string($item['name']) && trim($item['name']) !== '') { 936 $translated = self::maybeTranslateMeta($item['name'], $default, $lang); 937 if (is_string($translated) && $translated !== '' && $translated !== $item['name']) { 938 $item['name'] = $translated; 939 $changed = true; 940 } 941 } 942 } 943 unset($item); 944 } 945 946 // Handle @graph arrays (used by Yoast and others) 947 if (isset($node['@graph']) && is_array($node['@graph'])) { 948 foreach ($node['@graph'] as &$graphNode) { 949 if (is_array($graphNode)) { 950 if (self::translateJsonLdNode($graphNode, $default, $lang)) { 951 $changed = true; 952 } 953 } 954 } 955 unset($graphNode); 956 } return $changed; 957 } 845 958 } 846 847 -
ai-translate/trunk/includes/class-ai-sitemap.php
r3455468 r3461910 40 40 public static function register_providers() 41 41 { 42 $provider = new AI_Sitemap_Provider_Languages(); 43 wp_register_sitemap_provider('languages', $provider); 42 // Language homepages (/{lang}/) 43 wp_register_sitemap_provider('languages', new AI_Sitemap_Provider_Languages()); 44 45 // Translated posts/pages/CPT (/{lang}/{translated-slug}/) 46 wp_register_sitemap_provider('ai-translate-posts', new AI_Sitemap_Provider_Translated()); 44 47 } 45 48 … … 87 90 88 91 // ────────────────────────────────────────────── 92 // Translated post/page/CPT entries 93 // ────────────────────────────────────────────── 94 95 /** 96 * Get translated post entries for a specific language. 97 * 98 * Only includes posts that already have a translated slug in the slugs table. 99 * Does NOT trigger API calls. 100 * 101 * @param string $lang Language code (e.g. 'de', 'it'). 102 * @param int $limit Max entries to return. 103 * @param int $offset Offset for pagination. 104 * @return array[] Each entry has 'loc' (string) and optional 'lastmod' (string ISO 8601). 105 */ 106 public static function get_translated_entries($lang, $limit = 2000, $offset = 0) 107 { 108 global $wpdb; 109 $table = $wpdb->prefix . 'ai_translate_slugs'; 110 $col_lang = self::slug_lang_column(); 111 112 $public_types = get_post_types(['public' => true], 'names'); 113 $public_types = array_diff($public_types, ['attachment']); 114 if (empty($public_types)) { 115 $public_types = ['post', 'page']; 116 } 117 $placeholders = implode(', ', array_fill(0, count($public_types), '%s')); 118 119 $rows = $wpdb->get_results($wpdb->prepare( 120 "SELECT s.translated_slug, p.post_modified_gmt 121 FROM `{$table}` s 122 JOIN `{$wpdb->posts}` p ON p.ID = s.post_id 123 WHERE s.`{$col_lang}` = %s 124 AND p.post_status = 'publish' 125 AND p.post_type IN ({$placeholders}) 126 AND s.translated_slug != '' 127 ORDER BY s.post_id ASC 128 LIMIT %d OFFSET %d", 129 array_merge([$lang], array_values($public_types), [$limit, $offset]) 130 ), ARRAY_A); 131 132 if (!is_array($rows) || empty($rows)) { 133 return []; 134 } 135 136 $home = rtrim(home_url('/'), '/'); 137 $entries = []; 138 foreach ($rows as $row) { 139 $slug = ltrim($row['translated_slug'], '/'); 140 if ($slug === '') { 141 continue; 142 } 143 $entry = [ 144 'loc' => $home . '/' . $lang . '/' . $slug . '/', 145 ]; 146 if (!empty($row['post_modified_gmt']) && $row['post_modified_gmt'] !== '0000-00-00 00:00:00') { 147 $entry['lastmod'] = strtotime($row['post_modified_gmt']); 148 } 149 $entries[] = $entry; 150 } 151 152 return $entries; 153 } 154 155 /** 156 * Count translated post entries for a specific language. 157 * 158 * @param string $lang Language code. 159 * @return int 160 */ 161 public static function count_translated_entries($lang) 162 { 163 global $wpdb; 164 $table = $wpdb->prefix . 'ai_translate_slugs'; 165 $col_lang = self::slug_lang_column(); 166 167 $public_types = get_post_types(['public' => true], 'names'); 168 $public_types = array_diff($public_types, ['attachment']); 169 if (empty($public_types)) { 170 $public_types = ['post', 'page']; 171 } 172 $placeholders = implode(', ', array_fill(0, count($public_types), '%s')); 173 174 return (int) $wpdb->get_var($wpdb->prepare( 175 "SELECT COUNT(*) 176 FROM `{$table}` s 177 JOIN `{$wpdb->posts}` p ON p.ID = s.post_id 178 WHERE s.`{$col_lang}` = %s 179 AND p.post_status = 'publish' 180 AND p.post_type IN ({$placeholders}) 181 AND s.translated_slug != ''", 182 array_merge([$lang], array_values($public_types)) 183 )); 184 } 185 186 /** 187 * Get non-default languages that have at least one translated entry. 188 * 189 * @return string[] 190 */ 191 public static function get_sitemap_languages() 192 { 193 $default = AI_Lang::default(); 194 $enabled = AI_Lang::enabled(); 195 $detectable = AI_Lang::detectable(); 196 $langs = array_values(array_unique(array_merge($enabled, $detectable))); 197 198 $result = []; 199 foreach ($langs as $lang) { 200 $lang = strtolower((string) $lang); 201 if ($default !== null && $lang === strtolower((string) $default)) { 202 continue; 203 } 204 $result[] = $lang; 205 } 206 return $result; 207 } 208 209 /** 210 * Detect slug table language column name. 211 * 212 * Uses the transient set by AI_Slugs::detect_schema() to avoid extra SHOW COLUMNS queries. 213 * 214 * @return string Column name ('lang' or 'language_code'). 215 */ 216 private static function slug_lang_column() 217 { 218 $schema = get_transient('ai_tr_slugs_schema'); 219 return ($schema === 'original') ? 'language_code' : 'lang'; 220 } 221 222 // ────────────────────────────────────────────── 89 223 // Google XML Sitemaps plugin integration 90 224 // ────────────────────────────────────────────── 91 225 92 226 /** 93 * Hook: sm_build_index – add our sitemap to the Google XML Sitemaps index.227 * Hook: sm_build_index – add our sitemaps to the Google XML Sitemaps index. 94 228 * 95 229 * @param \GoogleSitemapGenerator $gsg … … 97 231 public static function gsm_build_index($gsg) 98 232 { 99 $entries = self::get_language_entries(); 100 if (empty($entries)) { 101 return; 102 } 103 104 $lastmod = 0; 105 foreach ($entries as $e) { 106 if (!empty($e['lastmod']) && $e['lastmod'] > $lastmod) { 107 $lastmod = $e['lastmod']; 108 } 109 } 110 111 $gsg->add_sitemap('languages-sitemap', null, $lastmod); 112 } 113 114 /** 115 * Hook: sm_build_content – output language homepage URLs when our type is requested. 116 * Google XML Sitemaps routes languages-sitemap.xml as type='pt', params='languages-p...' 233 // Language homepages 234 $lang_entries = self::get_language_entries(); 235 if (!empty($lang_entries)) { 236 $lastmod = 0; 237 foreach ($lang_entries as $e) { 238 if (!empty($e['lastmod']) && $e['lastmod'] > $lastmod) { 239 $lastmod = $e['lastmod']; 240 } 241 } 242 $gsg->add_sitemap('languages-sitemap', null, $lastmod); 243 } 244 245 // Translated posts per language 246 foreach (self::get_sitemap_languages() as $lang) { 247 $entries = self::get_translated_entries($lang, 1, 0); 248 if (!empty($entries)) { 249 $lastmod = !empty($entries[0]['lastmod']) ? $entries[0]['lastmod'] : 0; 250 $gsg->add_sitemap('translated-' . $lang . '-sitemap', null, $lastmod); 251 } 252 } 253 } 254 255 /** 256 * Hook: sm_build_content – output language homepage + translated post URLs. 117 257 * 118 258 * @param \GoogleSitemapGenerator $gsg … … 122 262 public static function gsm_build_content($gsg, $type, $params) 123 263 { 124 // The plugin routes unknown sitemap types as pt-{name}-p{page}-...125 $is_ ours= ($type === 'languages')264 // Language homepages sitemap 265 $is_lang = ($type === 'languages') 126 266 || ($type === 'pt' && is_string($params) && strpos($params, 'languages') === 0); 127 267 128 if (!$is_ours) { 268 if ($is_lang) { 269 foreach (self::get_language_entries() as $e) { 270 $gsg->add_url($e['loc'], $e['lastmod'], 'weekly', 0.5); 271 } 129 272 return; 130 273 } 131 274 132 $entries = self::get_language_entries(); 133 foreach ($entries as $e) { 134 $gsg->add_url( 135 $e['loc'], 136 $e['lastmod'], 137 'weekly', 138 0.5 139 ); 275 // Translated posts sitemaps: translated-{lang}-sitemap 276 $is_translated = ($type === 'pt' && is_string($params) && strpos($params, 'translated-') === 0); 277 if (!$is_translated) { 278 return; 279 } 280 281 // Extract language code from params: "translated-{lang}-sitemap..." 282 if (preg_match('/^translated-([a-z]{2,5})-/', $params, $m)) { 283 $lang = $m[1]; 284 $entries = self::get_translated_entries($lang, 50000, 0); 285 foreach ($entries as $e) { 286 $lastmod = !empty($e['lastmod']) ? $e['lastmod'] : 0; 287 $gsg->add_url($e['loc'], $lastmod, 'weekly', 0.5); 288 } 140 289 } 141 290 } … … 184 333 } 185 334 } 335 336 /** 337 * WP native sitemap provider for translated posts/pages/CPT. 338 * 339 * Registers one subtype per non-default language. Each subtype sitemap contains 340 * all translated posts for that language, using the translated slugs from the 341 * ai_translate_slugs table. 342 * 343 * Sitemap URLs look like: /wp-sitemap-ai-translate-posts-{lang}-{page}.xml 344 */ 345 class AI_Sitemap_Provider_Translated extends \WP_Sitemaps_Provider 346 { 347 public function __construct() 348 { 349 $this->name = 'ai-translate-posts'; 350 $this->object_type = 'ai-translate-posts'; 351 } 352 353 /** 354 * Return one subtype per non-default language. 355 * 356 * @return \stdClass[] 357 */ 358 public function get_object_subtypes() 359 { 360 $subtypes = []; 361 foreach (AI_Sitemap::get_sitemap_languages() as $lang) { 362 $obj = new \stdClass(); 363 $obj->name = $lang; 364 $subtypes[$lang] = $obj; 365 } 366 return $subtypes; 367 } 368 369 /** 370 * @param int $page_num 371 * @param string $object_subtype Language code. 372 * @return array[] 373 */ 374 public function get_url_list($page_num, $object_subtype = '') 375 { 376 $lang = sanitize_key($object_subtype); 377 if ($lang === '') { 378 return []; 379 } 380 381 $max_urls = wp_sitemaps_get_max_urls($this->object_type); 382 $offset = ($page_num - 1) * $max_urls; 383 384 $entries = AI_Sitemap::get_translated_entries($lang, $max_urls, $offset); 385 386 $urls = []; 387 foreach ($entries as $e) { 388 $entry = ['loc' => $e['loc']]; 389 if (!empty($e['lastmod'])) { 390 $entry['lastmod'] = wp_date('c', $e['lastmod']); 391 } 392 $urls[] = $entry; 393 } 394 return $urls; 395 } 396 397 /** 398 * @param string $object_subtype Language code. 399 * @return int 400 */ 401 public function get_max_num_pages($object_subtype = '') 402 { 403 $lang = sanitize_key($object_subtype); 404 if ($lang === '') { 405 return 0; 406 } 407 408 $count = AI_Sitemap::count_translated_entries($lang); 409 $max_urls = wp_sitemaps_get_max_urls($this->object_type); 410 411 return (int) ceil($count / $max_urls); 412 } 413 } -
ai-translate/trunk/includes/class-ai-translate-core.php
r3455695 r3461910 351 351 // If model is provided, test the API endpoint to ensure model is actually usable 352 352 if ($model !== '') { 353 // Early guard for model families that are known to be non-chat/non-translation models. 354 if (preg_match('/(dall-e|whisper|audio|image|realtime|transcribe|tts|embedding|moderation|codex|instruct)/i', $model)) { 355 throw new \Exception( 356 sprintf( 357 /* translators: %s is the selected model ID */ 358 __('Selected model "%s" is not a chat/completions model and cannot be used for translations. Choose a text chat model (for example: gpt-4o-mini, gpt-4.1-mini, deepseek-chat).', 'ai-translate'), 359 $model 360 ) 361 ); 362 } 363 353 364 $endpointPath = self::get_model_endpoint_path($model); 354 365 $chatEndpoint = rtrim($base, '/') . $endpointPath; … … 383 394 if ($chatCode !== 200) { 384 395 $chatBodyText = (string) wp_remote_retrieve_body($chatResp); 396 $chatErrorMessage = ''; 397 $chatErrorData = json_decode($chatBodyText, true); 398 if ( 399 is_array($chatErrorData) 400 && isset($chatErrorData['error']) 401 && is_array($chatErrorData['error']) 402 && isset($chatErrorData['error']['message']) 403 ) { 404 $chatErrorMessage = (string) $chatErrorData['error']['message']; 405 } 406 $haystack = strtolower($chatErrorMessage . ' ' . $chatBodyText); 407 $nonChatModelError = ( 408 strpos($haystack, 'not allowed to sample from this model') !== false 409 || strpos($haystack, 'not a chat model') !== false 410 || strpos($haystack, 'v1/completions') !== false 411 || strpos($haystack, 'completions endpoint') !== false 412 || strpos($haystack, 'does not support') !== false 413 || strpos($haystack, 'image generation') !== false 414 || strpos($haystack, 'embedding') !== false 415 || strpos($haystack, 'moderation') !== false 416 || strpos($haystack, 'audio') !== false 417 || strpos($haystack, 'codex') !== false 418 ); 419 if ($nonChatModelError) { 420 throw new \Exception( 421 sprintf( 422 /* translators: %s is the selected model ID */ 423 __('Selected model "%s" is not supported for chat-based translations. Choose a chat/completions model instead.', 'ai-translate'), 424 $model 425 ) 426 ); 427 } 385 428 // Auto-detect /responses API requirement and retry once 386 429 if ($chatCode === 404 && self::is_responses_api_error($chatBodyText)) {
Note: See TracChangeset
for help on using the changeset viewer.