PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.28.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.28.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / ai / class-language-resolver.php

class-language-resolver.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.28.0, at includes/ai/class-language-resolver.php

161 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AI Target Language Resolver
4 *
5 * Resolves the human-readable language ThinkRank should instruct the AI to
6 * write metadata / briefs in. Without this, the generation prompts carried no
7 * language directive and lighter models (e.g. GPT-5-mini) defaulted to English
8 * on non-English sites — emitting English or mixed-language titles and
9 * descriptions on, say, a Spanish news site. See GitHub issue #234.
10 *
11 * Resolution order:
12 * 1. The post's own language when a multilingual plugin (Polylang / WPML) is
13 * active and a post id is supplied — the accurate signal on sites whose
14 * content language differs per post.
15 * 2. The site locale, get_locale().
16 * 3. The `thinkrank_ai_target_language` filter, for explicit overrides on
17 * sites whose content language differs from the WordPress locale.
18 *
19 * @package ThinkRank\AI
20 * @since 1.27.0
21 */
22
23 declare(strict_types=1);
24
25 namespace ThinkRank\AI;
26
27 // Prevent direct access.
28 if (!defined('ABSPATH')) {
29 exit;
30 }
31
32 /**
33 * Resolves the AI output language for a given post/site.
34 *
35 * @since 1.27.0
36 */
37 class Language_Resolver {
38
39 /**
40 * Primary-subtag → English language name map for the locales WordPress
41 * ships translations for. Used as a dependency-free fallback; when the
42 * intl extension is present we prefer its richer display names.
43 *
44 * @var array<string,string>
45 */
46 private const LANGUAGE_NAMES = [
47 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French',
48 'de' => 'German', 'it' => 'Italian', 'pt' => 'Portuguese',
49 'nl' => 'Dutch', 'ru' => 'Russian', 'pl' => 'Polish',
50 'sv' => 'Swedish', 'da' => 'Danish', 'nb' => 'Norwegian',
51 'nn' => 'Norwegian', 'fi' => 'Finnish', 'cs' => 'Czech',
52 'sk' => 'Slovak', 'ro' => 'Romanian', 'hu' => 'Hungarian',
53 'el' => 'Greek', 'tr' => 'Turkish', 'ar' => 'Arabic',
54 'he' => 'Hebrew', 'fa' => 'Persian', 'hi' => 'Hindi',
55 'bn' => 'Bengali', 'ur' => 'Urdu', 'th' => 'Thai',
56 'vi' => 'Vietnamese', 'id' => 'Indonesian', 'ms' => 'Malay',
57 'ja' => 'Japanese', 'ko' => 'Korean', 'zh' => 'Chinese',
58 'uk' => 'Ukrainian', 'bg' => 'Bulgarian', 'hr' => 'Croatian',
59 'sr' => 'Serbian', 'sl' => 'Slovenian', 'lt' => 'Lithuanian',
60 'lv' => 'Latvian', 'et' => 'Estonian', 'ca' => 'Catalan',
61 'gl' => 'Galician', 'eu' => 'Basque', 'af' => 'Afrikaans',
62 'sw' => 'Swahili', 'tl' => 'Filipino', 'is' => 'Icelandic',
63 ];
64
65 /**
66 * Resolve the human-readable target language name for a post/site.
67 *
68 * @since 1.27.0
69 *
70 * @param int $post_id Post being optimized, or 0 for a site-level target.
71 * @return string Human-readable language name, e.g. "Spanish".
72 */
73 public static function resolve(int $post_id = 0): string {
74 $locale = self::resolve_locale($post_id);
75 $language = self::locale_to_name($locale);
76
77 /**
78 * Filter the language ThinkRank instructs the AI to write output in.
79 *
80 * Return a plain language name ("Spanish", "Brazilian Portuguese").
81 * An empty string disables the language directive entirely, restoring
82 * the pre-1.27.0 behavior of letting the model infer the language.
83 *
84 * @since 1.27.0
85 *
86 * @param string $language Human-readable language name.
87 * @param int $post_id Post being optimized (0 when site-level).
88 * @param string $locale Resolved locale, e.g. "es_ES".
89 */
90 return (string) apply_filters('thinkrank_ai_target_language', $language, $post_id, $locale);
91 }
92
93 /**
94 * Resolve the effective locale for a post/site.
95 *
96 * @since 1.27.0
97 *
98 * @param int $post_id Post id, or 0 for the site locale.
99 * @return string A WordPress locale, e.g. "es_ES".
100 */
101 public static function resolve_locale(int $post_id = 0): string {
102 if ($post_id > 0) {
103 // Polylang: gives the post's own language locale directly.
104 if (function_exists('pll_get_post_language')) {
105 $loc = pll_get_post_language($post_id, 'locale');
106 if (is_string($loc) && $loc !== '') {
107 return $loc;
108 }
109 }
110
111 // WPML: post language details carry a locale (and always a code).
112 if (has_filter('wpml_post_language_details')) {
113 $details = apply_filters('wpml_post_language_details', null, $post_id);
114 if (is_array($details)) {
115 if (!empty($details['locale']) && is_string($details['locale'])) {
116 return $details['locale'];
117 }
118 if (!empty($details['language_code']) && is_string($details['language_code'])) {
119 // Only a bare code (e.g. "es"); locale_to_name reads the
120 // primary subtag anyway, so passing the code is fine.
121 return $details['language_code'];
122 }
123 }
124 }
125 }
126
127 return get_locale();
128 }
129
130 /**
131 * Convert a locale (or bare language code) to an English language name.
132 *
133 * Falls back to the intl extension when present, then to the built-in map,
134 * then to the raw locale so the directive is never blank for a real locale.
135 *
136 * @since 1.27.0
137 *
138 * @param string $locale Locale or language code, e.g. "es_ES" or "es".
139 * @return string Language name, e.g. "Spanish".
140 */
141 public static function locale_to_name(string $locale): string {
142 $locale = str_replace('-', '_', trim($locale));
143 if ($locale === '') {
144 return '';
145 }
146
147 $subtag = strtolower(explode('_', $locale)[0]);
148
149 if (class_exists('\Locale')) {
150 $name = \Locale::getDisplayLanguage($locale, 'en');
151 // getDisplayLanguage echoes the input back when it can't resolve;
152 // treat that as a miss and fall through to the map.
153 if (is_string($name) && $name !== '' && strtolower($name) !== strtolower($locale) && strtolower($name) !== $subtag) {
154 return $name;
155 }
156 }
157
158 return self::LANGUAGE_NAMES[$subtag] ?? $locale;
159 }
160 }
161