| @@ -28,8 +28,9 @@ | ||
| 28 | 28 | */ |
| 29 | 29 | public function __construct() { |
| 30 | 30 | Hook_Registry::add_filter( 'top_ten_query_the_posts', array( $this, 'translate_ids' ), 999 ); |
| 31 | 31 | Hook_Registry::add_filter( 'top_ten_query_posts_pre_query', array( $this, 'translate_ids' ), 999 ); |
| 32 | + Hook_Registry::add_filter( 'rest_pre_echo_response', array( $this, 'translate_rest_response' ), 10, 3 ); | |
| 32 | 33 | } |
| 33 | 34 | |
| 34 | 35 | /** |
| 35 | 36 | * Get the ID of a post in the current language. Works with WPML and PolyLang. |
| @@ -112,6 +113,379 @@ | ||
| 112 | 113 | * |
| 113 | 114 | * @param \WP_Post|array|null $id Post object. |
| 114 | 115 | */ |
| 115 | 116 | return apply_filters( 'tptn_object_id_cur_lang', $post ); |
| 117 | + } | |
| 118 | + | |
| 119 | + /** | |
| 120 | + * Whether TranslatePress is active and exposes the API this integration needs. | |
| 121 | + * | |
| 122 | + * @since 4.5.1 | |
| 123 | + * | |
| 124 | + * @return bool True if TranslatePress can be used. | |
| 125 | + */ | |
| 126 | + public static function is_translatepress_active(): bool { | |
| 127 | + return function_exists( 'trp_translate' ) && class_exists( 'TRP_Translate_Press' ); | |
| 128 | + } | |
| 129 | + | |
| 130 | + /** | |
| 131 | + * Fetch a TranslatePress component instance. | |
| 132 | + * | |
| 133 | + * @since 4.5.1 | |
| 134 | + * | |
| 135 | + * @param string $component Component name, e.g. `url_converter`. | |
| 136 | + * @return object|null Component instance or null when unavailable. | |
| 137 | + */ | |
| 138 | + public static function get_trp_component( string $component ) { | |
| 139 | + if ( ! self::is_translatepress_active() ) { | |
| 140 | + return null; | |
| 141 | + } | |
| 142 | + | |
| 143 | + $trp = \TRP_Translate_Press::get_trp_instance(); | |
| 144 | + if ( ! is_object( $trp ) || ! method_exists( $trp, 'get_component' ) ) { | |
| 145 | + return null; | |
| 146 | + } | |
| 147 | + | |
| 148 | + $instance = $trp->get_component( $component ); | |
| 149 | + | |
| 150 | + return is_object( $instance ) ? $instance : null; | |
| 151 | + } | |
| 152 | + | |
| 153 | + /** | |
| 154 | + * Get the TranslatePress settings array. | |
| 155 | + * | |
| 156 | + * @since 4.5.1 | |
| 157 | + * | |
| 158 | + * @return array TranslatePress settings. | |
| 159 | + */ | |
| 160 | + public static function get_trp_settings(): array { | |
| 161 | + $settings = get_option( 'trp_settings', array() ); | |
| 162 | + | |
| 163 | + return is_array( $settings ) ? $settings : array(); | |
| 164 | + } | |
| 165 | + | |
| 166 | + /** | |
| 167 | + * Get the TranslatePress language the current front-end request is rendering in. | |
| 168 | + * | |
| 169 | + * @since 4.5.1 | |
| 170 | + * | |
| 171 | + * @return string Language code, or an empty string when TranslatePress is inactive | |
| 172 | + * or the request is in the default language. | |
| 173 | + */ | |
| 174 | + public static function get_trp_current_language(): string { | |
| 175 | + if ( ! self::is_translatepress_active() ) { | |
| 176 | + return ''; | |
| 177 | + } | |
| 178 | + | |
| 179 | + global $TRP_LANGUAGE; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- TranslatePress global. | |
| 180 | + | |
| 181 | + $language = is_string( $TRP_LANGUAGE ) ? $TRP_LANGUAGE : ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- TranslatePress global. | |
| 182 | + $settings = self::get_trp_settings(); | |
| 183 | + $default = isset( $settings['default-language'] ) ? (string) $settings['default-language'] : ''; | |
| 184 | + | |
| 185 | + return ( '' === $language || $language === $default ) ? '' : $language; | |
| 186 | + } | |
| 187 | + | |
| 188 | + /** | |
| 189 | + * Get a language identifier for cache keys. | |
| 190 | + * | |
| 191 | + * Rendered output is language-specific — TranslatePress filters `home_url()`, and | |
| 192 | + * WPML/Polylang resolve different post IDs — so cached HTML and post lists must not | |
| 193 | + * be shared between languages. | |
| 194 | + * | |
| 195 | + * @since 4.5.1 | |
| 196 | + * | |
| 197 | + * @return string Current language code, or an empty string when the site is monolingual. | |
| 198 | + */ | |
| 199 | + public static function get_cache_language(): string { | |
| 200 | + $language = self::get_trp_current_language(); | |
| 201 | + | |
| 202 | + if ( '' === $language && function_exists( 'pll_current_language' ) ) { | |
| 203 | + $language = (string) \pll_current_language( 'locale' ); | |
| 204 | + } | |
| 205 | + | |
| 206 | + if ( '' === $language && class_exists( 'SitePress' ) ) { | |
| 207 | + $language = (string) apply_filters( 'wpml_current_language', null ); | |
| 208 | + } | |
| 209 | + | |
| 210 | + /** | |
| 211 | + * Filters the language component added to Top 10 cache keys. | |
| 212 | + * | |
| 213 | + * @since 4.5.1 | |
| 214 | + * | |
| 215 | + * @param string $language Current language code, or an empty string. | |
| 216 | + */ | |
| 217 | + return (string) apply_filters( 'tptn_cache_language', $language ); | |
| 218 | + } | |
| 219 | + | |
| 220 | + /** | |
| 221 | + * Get the referring URL, but only when it points at this site. | |
| 222 | + * | |
| 223 | + * TranslatePress resolves a language from any URL containing a known slug, so an | |
| 224 | + * off-site referer would otherwise let a cross-site request choose the language. | |
| 225 | + * | |
| 226 | + * @since 4.5.1 | |
| 227 | + * | |
| 228 | + * @return string Referring URL on this host, or an empty string. | |
| 229 | + */ | |
| 230 | + protected static function get_same_origin_referer(): string { | |
| 231 | + $referer = function_exists( 'wp_get_raw_referer' ) ? wp_get_raw_referer() : ''; | |
| 232 | + | |
| 233 | + if ( ! is_string( $referer ) || '' === $referer ) { | |
| 234 | + return ''; | |
| 235 | + } | |
| 236 | + | |
| 237 | + $referer_host = wp_parse_url( $referer, PHP_URL_HOST ); | |
| 238 | + $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); | |
| 239 | + | |
| 240 | + if ( empty( $referer_host ) || empty( $home_host ) || strtolower( (string) $referer_host ) !== strtolower( (string) $home_host ) ) { | |
| 241 | + return ''; | |
| 242 | + } | |
| 243 | + | |
| 244 | + return $referer; | |
| 245 | + } | |
| 246 | + | |
| 247 | + /** | |
| 248 | + * Resolve the TranslatePress language a REST request should be rendered in. | |
| 249 | + * | |
| 250 | + * TranslatePress derives the language from the page URL, but REST routes carry no | |
| 251 | + * language prefix, so the language is taken from an explicit `lang` parameter and | |
| 252 | + * falls back to the referring front-end URL. | |
| 253 | + * | |
| 254 | + * @since 4.5.1 | |
| 255 | + * | |
| 256 | + * @param \WP_REST_Request|null $request REST request. | |
| 257 | + * @return string Language code, or an empty string when no translation is needed. | |
| 258 | + */ | |
| 259 | + public static function get_trp_rest_language( $request = null ): string { | |
| 260 | + if ( ! self::is_translatepress_active() ) { | |
| 261 | + return ''; | |
| 262 | + } | |
| 263 | + | |
| 264 | + $settings = self::get_trp_settings(); | |
| 265 | + $default = isset( $settings['default-language'] ) ? (string) $settings['default-language'] : ''; | |
| 266 | + | |
| 267 | + // Mirrors TRP_Url_Converter::get_lang_from_url_string(): unpublished languages are for translators only. | |
| 268 | + $available = isset( $settings['publish-languages'] ) ? (array) $settings['publish-languages'] : array(); | |
| 269 | + | |
| 270 | + if ( current_user_can( (string) apply_filters( 'trp_translating_capability', 'manage_options' ) ) ) { | |
| 271 | + $available = array_merge( $available, isset( $settings['translation-languages'] ) ? (array) $settings['translation-languages'] : array() ); | |
| 272 | + } | |
| 273 | + $available = array_values( array_unique( array_map( 'strval', $available ) ) ); | |
| 274 | + | |
| 275 | + $language = ''; | |
| 276 | + | |
| 277 | + if ( $request instanceof \WP_REST_Request ) { | |
| 278 | + $raw_language = $request->get_param( 'lang' ); | |
| 279 | + $language = is_string( $raw_language ) ? sanitize_text_field( $raw_language ) : ''; | |
| 280 | + } | |
| 281 | + | |
| 282 | + // Accept either a TranslatePress locale (fr_FR) or its URL slug (fr). | |
| 283 | + $url_slugs = isset( $settings['url-slugs'] ) && is_array( $settings['url-slugs'] ) ? $settings['url-slugs'] : array(); | |
| 284 | + foreach ( $url_slugs as $locale => $slug ) { | |
| 285 | + if ( is_string( $slug ) && $slug === $language ) { | |
| 286 | + $language = (string) $locale; | |
| 287 | + break; | |
| 288 | + } | |
| 289 | + } | |
| 290 | + | |
| 291 | + if ( '' === $language ) { | |
| 292 | + $url_converter = self::get_trp_component( 'url_converter' ); | |
| 293 | + | |
| 294 | + if ( $url_converter && method_exists( $url_converter, 'get_lang_from_url_string' ) ) { | |
| 295 | + // TranslatePress filters home_url(), so rest_url() carries the language prefix of the page that enqueued it. | |
| 296 | + if ( method_exists( $url_converter, 'cur_page_url' ) ) { | |
| 297 | + $language = (string) $url_converter->get_lang_from_url_string( $url_converter->cur_page_url() ); | |
| 298 | + } | |
| 299 | + | |
| 300 | + if ( '' === $language ) { | |
| 301 | + $referer = self::get_same_origin_referer(); | |
| 302 | + | |
| 303 | + if ( '' !== $referer ) { | |
| 304 | + $language = (string) $url_converter->get_lang_from_url_string( $referer ); | |
| 305 | + } | |
| 306 | + } | |
| 307 | + } | |
| 308 | + } | |
| 309 | + | |
| 310 | + /** | |
| 311 | + * Filters the TranslatePress language used to render Top 10 REST responses. | |
| 312 | + * | |
| 313 | + * @since 4.5.1 | |
| 314 | + * | |
| 315 | + * @param string $language Language code resolved from the request. | |
| 316 | + * @param \WP_REST_Request|null $request REST request. | |
| 317 | + */ | |
| 318 | + $language = (string) apply_filters( 'tptn_trp_rest_language', $language, $request ); | |
| 319 | + | |
| 320 | + if ( '' === $language || $language === $default || ! in_array( $language, $available, true ) ) { | |
| 321 | + return ''; | |
| 322 | + } | |
| 323 | + | |
| 324 | + return $language; | |
| 325 | + } | |
| 326 | + | |
| 327 | + /** | |
| 328 | + * Translate a string with TranslatePress. | |
| 329 | + * | |
| 330 | + * @since 4.5.1 | |
| 331 | + * | |
| 332 | + * @param string $content Content in the default language. Text or HTML. | |
| 333 | + * @param string $language Target language code. | |
| 334 | + * @return string Translated content. | |
| 335 | + */ | |
| 336 | + public static function trp_translate_content( string $content, string $language ): string { | |
| 337 | + if ( '' === $content || '' === $language || ! self::is_translatepress_active() ) { | |
| 338 | + return $content; | |
| 339 | + } | |
| 340 | + | |
| 341 | + // $prevent_over_translation wraps the return in a span; REST output never passes through TranslatePress's page buffer. | |
| 342 | + return (string) \trp_translate( $content, $language, false ); | |
| 343 | + } | |
| 344 | + | |
| 345 | + /** | |
| 346 | + * Convert a URL to its TranslatePress equivalent in the given language. | |
| 347 | + * | |
| 348 | + * @since 4.5.1 | |
| 349 | + * | |
| 350 | + * @param string $url URL in the default language. | |
| 351 | + * @param string $language Target language code. | |
| 352 | + * @return string Converted URL. | |
| 353 | + */ | |
| 354 | + public static function trp_translate_url( string $url, string $language ): string { | |
| 355 | + if ( '' === $url || '' === $language ) { | |
| 356 | + return $url; | |
| 357 | + } | |
| 358 | + | |
| 359 | + $url_converter = self::get_trp_component( 'url_converter' ); | |
| 360 | + | |
| 361 | + if ( ! $url_converter || ! method_exists( $url_converter, 'get_url_for_language' ) ) { | |
| 362 | + return $url; | |
| 363 | + } | |
| 364 | + | |
| 365 | + $converted = $url_converter->get_url_for_language( $language, $url, '' ); | |
| 366 | + | |
| 367 | + return is_string( $converted ) && '' !== $converted ? $converted : $url; | |
| 368 | + } | |
| 369 | + | |
| 370 | + /** | |
| 371 | + * Translate the Top 10 REST API response with TranslatePress. | |
| 372 | + * | |
| 373 | + * TranslatePress's own page output buffer never runs for REST requests, so the | |
| 374 | + * output returned over REST would otherwise always be served in the default | |
| 375 | + * language. | |
| 376 | + * | |
| 377 | + * @since 4.5.1 | |
| 378 | + * | |
| 379 | + * @param mixed $result Response data to send to the client. | |
| 380 | + * @param mixed $server Server instance. | |
| 381 | + * @param mixed $request Request used to generate the response. | |
| 382 | + * @return mixed Response data, translated where applicable. | |
| 383 | + */ | |
| 384 | + public static function translate_rest_response( $result, $server, $request ) { | |
| 385 | + if ( ! $request instanceof \WP_REST_Request ) { | |
| 386 | + return $result; | |
| 387 | + } | |
| 388 | + | |
| 389 | + if ( ! is_array( $result ) && ! is_string( $result ) ) { | |
| 390 | + return $result; | |
| 391 | + } | |
| 392 | + | |
| 393 | + $route = (string) $request->get_route(); | |
| 394 | + $namespace = '/' . self::get_rest_namespace(); | |
| 395 | + if ( $route !== $namespace && 0 !== strpos( $route, $namespace . '/' ) ) { | |
| 396 | + return $result; | |
| 397 | + } | |
| 398 | + | |
| 399 | + $language = self::get_trp_rest_language( $request ); | |
| 400 | + | |
| 401 | + if ( '' === $language ) { | |
| 402 | + return $result; | |
| 403 | + } | |
| 404 | + | |
| 405 | + // Some routes return a bare HTML string rather than a structure of fields. | |
| 406 | + if ( is_string( $result ) ) { | |
| 407 | + return self::trp_translate_content( $result, $language ); | |
| 408 | + } | |
| 409 | + | |
| 410 | + /** | |
| 411 | + * Filters the response keys Top 10 translates with TranslatePress. | |
| 412 | + * | |
| 413 | + * @since 4.5.1 | |
| 414 | + * | |
| 415 | + * @param array $keys Associative array of `content` and `url` key names. | |
| 416 | + */ | |
| 417 | + $keys = apply_filters( | |
| 418 | + 'tptn_trp_rest_translatable_keys', | |
| 419 | + array( | |
| 420 | + 'content' => array( 'html', 'title', 'excerpt', 'content', 'name', 'description' ), | |
| 421 | + // `guid` is deliberately absent: it is an immutable identifier, not a navigable URL. | |
| 422 | + 'url' => array( 'link', 'permalink' ), | |
| 423 | + ) | |
| 424 | + ); | |
| 425 | + $keys = (array) $keys; | |
| 426 | + $keys = array( | |
| 427 | + 'content' => isset( $keys['content'] ) && is_array( $keys['content'] ) ? array_values( array_map( 'strval', array_filter( $keys['content'], 'is_scalar' ) ) ) : array(), | |
| 428 | + 'url' => isset( $keys['url'] ) && is_array( $keys['url'] ) ? array_values( array_map( 'strval', array_filter( $keys['url'], 'is_scalar' ) ) ) : array(), | |
| 429 | + ); | |
| 430 | + | |
| 431 | + return self::translate_rest_data( $result, $language, $keys ); | |
| 432 | + } | |
| 433 | + | |
| 434 | + /** | |
| 435 | + * The REST namespace whose responses this plugin translates. | |
| 436 | + * | |
| 437 | + * @since 4.5.1 | |
| 438 | + * | |
| 439 | + * @return string REST namespace. | |
| 440 | + */ | |
| 441 | + protected static function get_rest_namespace(): string { | |
| 442 | + return 'top-10/v1'; | |
| 443 | + } | |
| 444 | + | |
| 445 | + /** | |
| 446 | + * Walk a Top 10 REST payload translating the fields TranslatePress can handle. | |
| 447 | + * | |
| 448 | + * @since 4.5.1 | |
| 449 | + * | |
| 450 | + * @param array $data Response data. | |
| 451 | + * @param string $language Target language code. | |
| 452 | + * @param array $keys Translatable `content` and `url` key names. | |
| 453 | + * @param int $depth Current recursion depth. | |
| 454 | + * @return array Translated response data. | |
| 455 | + */ | |
| 456 | + protected static function translate_rest_data( array $data, string $language, array $keys, int $depth = 0 ): array { | |
| 457 | + if ( $depth > 5 ) { | |
| 458 | + return $data; | |
| 459 | + } | |
| 460 | + | |
| 461 | + foreach ( $data as $key => $value ) { | |
| 462 | + if ( is_array( $value ) ) { | |
| 463 | + if ( isset( $value['rendered'] ) && is_string( $value['rendered'] ) ) { | |
| 464 | + if ( in_array( (string) $key, $keys['content'], true ) ) { | |
| 465 | + $data[ $key ]['rendered'] = self::trp_translate_content( $value['rendered'], $language ); | |
| 466 | + continue; | |
| 467 | + } | |
| 468 | + if ( in_array( (string) $key, $keys['url'], true ) ) { | |
| 469 | + $data[ $key ]['rendered'] = self::trp_translate_url( $value['rendered'], $language ); | |
| 470 | + continue; | |
| 471 | + } | |
| 472 | + } | |
| 473 | + | |
| 474 | + $data[ $key ] = self::translate_rest_data( $value, $language, $keys, $depth + 1 ); | |
| 475 | + continue; | |
| 476 | + } | |
| 477 | + | |
| 478 | + if ( ! is_string( $value ) || '' === $value ) { | |
| 479 | + continue; | |
| 480 | + } | |
| 481 | + | |
| 482 | + if ( in_array( (string) $key, $keys['content'], true ) ) { | |
| 483 | + $data[ $key ] = self::trp_translate_content( $value, $language ); | |
| 484 | + } elseif ( in_array( (string) $key, $keys['url'], true ) ) { | |
| 485 | + $data[ $key ] = self::trp_translate_url( $value, $language ); | |
| 486 | + } | |
| 487 | + } | |
| 488 | + | |
| 489 | + return $data; | |
| 116 | 490 | } |
| 117 | 491 | } |