[ Index ] |
PHP Cross Reference of GlotPress |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Translation warnings API 4 * 5 * @package GlotPress 6 * @since 1.0.0 7 */ 8 9 /** 10 * Class used to handle translation warnings. 11 * 12 * @since 1.0.0 13 */ 14 class GP_Translation_Warnings { 15 16 /** 17 * List of callbacks. 18 * 19 * @since 1.0.0 20 * @access public 21 * 22 * @var callable[] 23 */ 24 public $callbacks = array(); 25 26 /** 27 * Adds a callback for a new warning. 28 * 29 * @since 1.0.0 30 * @access public 31 * 32 * @param string $id Unique ID of the callback. 33 * @param callable $callback The callback. 34 */ 35 public function add( $id, $callback ) { 36 $this->callbacks[ $id ] = $callback; 37 } 38 39 /** 40 * Removes an existing callback for a warning. 41 * 42 * @since 1.0.0 43 * @access public 44 * 45 * @param string $id Unique ID of the callback. 46 */ 47 public function remove( $id ) { 48 unset( $this->callbacks[ $id ] ); 49 } 50 51 /** 52 * Checks whether a callback exists for an ID. 53 * 54 * @since 1.0.0 55 * @access public 56 * 57 * @param string $id Unique ID of the callback. 58 * @return bool True if exists, false if not. 59 */ 60 public function has( $id ) { 61 return isset( $this->callbacks[ $id ] ); 62 } 63 64 /** 65 * Checks translations for any issues/warnings. 66 * 67 * @since 1.0.0 68 * @access public 69 * 70 * @param string $singular The singular form of an original string. 71 * @param string $plural The plural form of an original string. 72 * @param string[] $translations An array of translations for an original. 73 * @param GP_Locale $locale The locale of the translations. 74 * @return array|null Null if no issues have been found, otherwise an array 75 * with warnings. 76 */ 77 public function check( $singular, $plural, $translations, $locale ) { 78 $problems = array(); 79 foreach ( $translations as $translation_index => $translation ) { 80 if ( ! $translation ) { 81 continue; 82 } 83 84 $skip = array( 85 'singular' => false, 86 'plural' => false, 87 ); 88 if ( null !== $plural ) { 89 $numbers_for_index = $locale->numbers_for_index( $translation_index ); 90 if ( 1 === $locale->nplurals ) { 91 $skip['singular'] = true; 92 } elseif ( in_array( 1, $numbers_for_index, true ) ) { 93 $skip['plural'] = true; 94 } else { 95 $skip['singular'] = true; 96 } 97 } 98 99 foreach ( $this->callbacks as $callback_id => $callback ) { 100 if ( ! $skip['singular'] ) { 101 $singular_test = $callback( $singular, $translation, $locale ); 102 if ( true !== $singular_test ) { 103 $problems[ $translation_index ][ $callback_id ] = $singular_test; 104 } 105 } 106 if ( null !== $plural && ! $skip['plural'] ) { 107 $plural_test = $callback( $plural, $translation, $locale ); 108 if ( true !== $plural_test ) { 109 $problems[ $translation_index ][ $callback_id ] = $plural_test; 110 } 111 } 112 } 113 } 114 115 return empty( $problems ) ? null : $problems; 116 } 117 } 118 119 /** 120 * Class used to register built-in translation warnings. 121 * 122 * @since 1.0.0 123 */ 124 class GP_Builtin_Translation_Warnings { 125 126 /** 127 * Lower bound for length checks. 128 * 129 * @since 1.0.0 130 * @access public 131 * 132 * @var float 133 */ 134 public $length_lower_bound = 0.2; 135 136 /** 137 * Upper bound for length checks. 138 * 139 * @since 1.0.0 140 * @access public 141 * 142 * @var float 143 */ 144 public $length_upper_bound = 5.0; 145 146 /** 147 * List of locales which are excluded from length checks. 148 * 149 * @since 1.0.0 150 * @access public 151 * 152 * @var array 153 */ 154 public $length_exclude_languages = array( 'art-xemoji', 'ja', 'ko', 'zh', 'zh-hk', 'zh-cn', 'zh-sg', 'zh-tw' ); 155 156 /** 157 * List of domains with allowed changes to their own subdomains 158 * 159 * @since 3.0.0 160 * @access public 161 * 162 * @var array 163 */ 164 public $allowed_domain_changes = array( 165 // Allow links to wordpress.org to be changed to a subdomain. 166 'wordpress.org' => '[^.]+\.wordpress\.org', 167 // Allow links to wordpress.com to be changed to a subdomain. 168 'wordpress.com' => '[^.]+\.wordpress\.com', 169 // Allow links to gravatar.org to be changed to a subdomain. 170 'en.gravatar.com' => '[^.]+\.gravatar\.com', 171 // Allow links to wikipedia.org to be changed to a subdomain. 172 'en.wikipedia.org' => '[^.]+\.wikipedia\.org', 173 ); 174 175 /** 176 * List of languages without italics 177 * 178 * @since 3.0.0 179 * @access public 180 * 181 * @var array 182 */ 183 public $languages_without_italics = array( 184 'ja', 185 'ko', 186 'zh', 187 'zh-hk', 188 'zh-cn', 189 'zh-sg', 190 'zh-tw', 191 ); 192 193 /** 194 * Checks whether lengths of source and translation differ too much. 195 * 196 * @since 1.0.0 197 * @access public 198 * 199 * @param string $original The source string. 200 * @param string $translation The translation. 201 * @param GP_Locale $locale The locale of the translation. 202 * @return string|true True if check is OK, otherwise warning message. 203 */ 204 public function warning_length( $original, $translation, $locale ) { 205 if ( in_array( $locale->slug, $this->length_exclude_languages, true ) ) { 206 return true; 207 } 208 209 if ( gp_startswith( $original, 'number_format_' ) ) { 210 return true; 211 } 212 213 $len_src = mb_strlen( $original ); 214 $len_trans = mb_strlen( $translation ); 215 if ( 216 ! ( 217 $this->length_lower_bound * $len_src < $len_trans && 218 $len_trans < $this->length_upper_bound * $len_src 219 ) && 220 ( 221 ! gp_in( '_abbreviation', $original ) && 222 ! gp_in( '_initial', $original ) ) 223 ) { 224 return __( 'Lengths of source and translation differ too much.', 'glotpress' ); 225 } 226 227 return true; 228 } 229 230 /** 231 * Checks whether HTML tags are missing or have been added. 232 * 233 * @todo Validate if the HTML is in the same order in function of the language. Validate nesting of HTML is same. 234 * 235 * @since 1.0.0 236 * @access public 237 * 238 * @param string $original The source string. 239 * @param string $translation The translation. 240 * @param GP_Locale $locale The locale of the translation. 241 * @return string|true True if check is OK, otherwise warning message. 242 */ 243 public function warning_tags( $original, $translation, $locale ) { 244 $tag_pattern = '(<[^>]*>)'; 245 $tag_re = "/$tag_pattern/Us"; 246 $original_parts = array(); 247 $translation_parts = array(); 248 249 if ( preg_match_all( $tag_re, $original, $m ) ) { 250 $original_parts = $m[1]; 251 } 252 if ( preg_match_all( $tag_re, $translation, $m ) ) { 253 $translation_parts = $m[1]; 254 } 255 256 // East asian languages can remove emphasis/italic tags. 257 if ( count( $original_parts ) > count( $translation_parts ) ) { 258 // Remove Italic requirements. 259 if ( in_array( $locale->slug, $this->languages_without_italics, true ) ) { 260 $original_parts = array_diff( $original_parts, array( '<em>', '</em>', '<i>', '</i>' ) ); 261 } 262 } 263 264 if ( count( $original_parts ) > count( $translation_parts ) ) { 265 return sprintf( 266 /* translators: %s: HTML tags. */ 267 __( 'Missing tags from translation. Expected: %s', 'glotpress' ), 268 implode( ' ', array_diff( $original_parts, $translation_parts ) ) 269 ); 270 } 271 if ( count( $original_parts ) < count( $translation_parts ) ) { 272 return sprintf( 273 /* translators: %s: HTML tags. */ 274 __( 'Too many tags in translation. Found: %s', 'glotpress' ), 275 implode( ' ', array_diff( $translation_parts, $original_parts ) ) 276 ); 277 } 278 279 // Check if the translation tags are in correct order. 280 $valid_html_warning = $this->check_valid_html( $original_parts, $translation_parts ); 281 if ( true !== $valid_html_warning ) { 282 return trim( $valid_html_warning ); 283 } 284 285 // Sort the tags, from this point out as long as all the tags are present is okay. 286 rsort( $original_parts ); 287 rsort( $translation_parts ); 288 289 $changeable_attributes = array( 290 // We allow certain attributes to be different in translations. 291 'title', 292 'aria-label', 293 // src and href will be checked separately. 294 'src', 295 'href', 296 ); 297 298 $attribute_regex = '/(\s*(?P<attr>%s))=([\'"])(?P<value>.+)\\3(\s*)/i'; 299 $attribute_replace = '$1=$3...$3$5'; 300 $changeable_attr_regex = sprintf( $attribute_regex, implode( '|', $changeable_attributes ) ); 301 302 // Items are sorted, so if all is well, will match up. 303 $parts_tags = array_combine( $original_parts, $translation_parts ); 304 305 $warnings = array(); 306 foreach ( $parts_tags as $original_tag => $translation_tag ) { 307 if ( $original_tag === $translation_tag ) { 308 continue; 309 } 310 311 // Remove any attributes that can be expected to differ. 312 $original_filtered_tag = preg_replace( $changeable_attr_regex, $attribute_replace, $original_tag ); 313 $translation_filtered_tag = preg_replace( $changeable_attr_regex, $attribute_replace, $translation_tag ); 314 315 if ( $original_filtered_tag !== $translation_filtered_tag ) { 316 $warnings[] = sprintf( 317 /* translators: 1: Original HTML tag. 2: Translated HTML tag. */ 318 __( 'Expected %1$s, got %2$s.', 'glotpress' ), 319 $original_tag, 320 $translation_tag 321 ); 322 } 323 } 324 325 // Now check that the URLs mentioned within href & src tags match. 326 $original_links = ''; 327 $translation_links = ''; 328 329 $original_links = implode( "\n", $this->get_values_from_href_src( $original_parts ) ); 330 $translation_links = implode( "\n", $this->get_values_from_href_src( $translation_parts ) ); 331 // Validate the URLs if present. 332 if ( $original_links || $translation_links ) { 333 $url_warnings = $this->links_without_url_and_placeholders_are_equal( $original_links, $translation_links ); 334 if ( true !== $url_warnings ) { 335 $warnings = array_merge( $warnings, $url_warnings ); 336 } 337 338 $url_warnings = $this->warning_mismatching_urls( $original_links, $translation_links ); 339 340 if ( true !== $url_warnings ) { 341 $warnings[] = $url_warnings; 342 } 343 } 344 345 if ( empty( $warnings ) ) { 346 return true; 347 } 348 349 return implode( "\n", $warnings ); 350 } 351 352 /** 353 * Checks whether PHP placeholders are missing or have been added. 354 * 355 * The default regular expression: 356 * bcdefgosuxEFGX are standard printf placeholders. 357 * % is included to allow/expect %%. 358 * l is included for wp_sprintf_l()'s custom %l format. 359 * @ is included for Swift (as used for iOS mobile app) %@ string format. 360 * 361 * @since 1.0.0 362 * @access public 363 * 364 * @param string $original The source string. 365 * @param string $translation The translation. 366 * @param GP_Locale $locale The locale of the translation. 367 * @return string|true True if check is OK, otherwise warning message. 368 */ 369 public function warning_placeholders( $original, $translation, $locale ) { 370 /** 371 * Filter the regular expression that is used to match placeholders in translations. 372 * 373 * @since 1.0.0 374 * 375 * @param string $placeholders_re Regular expression pattern without leading or trailing slashes. 376 */ 377 $placeholders_re = apply_filters( 'gp_warning_placeholders_re', '(?<!%)%(\d+\$(?:\d+)?)?(\.\d+)?[bcdefgosuxEFGX%l@]' ); 378 379 $original_counts = $this->_placeholders_counts( $original, $placeholders_re ); 380 $translation_counts = $this->_placeholders_counts( $translation, $placeholders_re ); 381 $all_placeholders = array_unique( array_merge( array_keys( $original_counts ), array_keys( $translation_counts ) ) ); 382 foreach ( $all_placeholders as $placeholder ) { 383 $original_count = gp_array_get( $original_counts, $placeholder, 0 ); 384 $translation_count = gp_array_get( $translation_counts, $placeholder, 0 ); 385 if ( $original_count > $translation_count ) { 386 return sprintf( 387 /* translators: %s: Placeholder. */ 388 __( 'Missing %s placeholder in translation.', 'glotpress' ), 389 $placeholder 390 ); 391 } 392 if ( $original_count < $translation_count ) { 393 return sprintf( 394 /* translators: %s: Placeholder. */ 395 __( 'Extra %s placeholder in translation.', 'glotpress' ), 396 $placeholder 397 ); 398 } 399 } 400 401 return true; 402 } 403 404 /** 405 * Counts the placeholders in a string. 406 * 407 * @since 1.0.0 408 * @access private 409 * 410 * @param string $string The string to search. 411 * @param string $re Regular expressions to match placeholders. 412 * @return array An array with counts per placeholder. 413 */ 414 private function _placeholders_counts( $string, $re ) { 415 $counts = array(); 416 preg_match_all( "/$re/", $string, $matches ); 417 foreach ( $matches[0] as $match ) { 418 $counts[ $match ] = gp_array_get( $counts, $match, 0 ) + 1; 419 } 420 421 return $counts; 422 } 423 424 /** 425 * Checks whether a translation does begin on newline. 426 * 427 * @since 1.0.0 428 * @access public 429 * 430 * @param string $original The source string. 431 * @param string $translation The translation. 432 * @param GP_Locale $locale The locale of the translation. 433 * @return string|true True if check is OK, otherwise warning message. 434 */ 435 public function warning_should_begin_on_newline( $original, $translation, $locale ) { 436 if ( gp_startswith( $original, "\n" ) && ! gp_startswith( $translation, "\n" ) ) { 437 return __( 'Original and translation should both begin on newline.', 'glotpress' ); 438 } 439 440 return true; 441 } 442 443 /** 444 * Checks whether a translation doesn't begin on newline. 445 * 446 * @since 1.0.0 447 * @access public 448 * 449 * @param string $original The source string. 450 * @param string $translation The translation. 451 * @param GP_Locale $locale The locale of the translation. 452 * @return string|true True if check is OK, otherwise warning message. 453 */ 454 public function warning_should_not_begin_on_newline( $original, $translation, $locale ) { 455 if ( ! gp_startswith( $original, "\n" ) && gp_startswith( $translation, "\n" ) ) { 456 return __( 'Translation should not begin on newline.', 'glotpress' ); 457 } 458 459 return true; 460 } 461 462 /** 463 * Checks whether a translation does end on newline. 464 * 465 * @since 1.0.0 466 * @access public 467 * 468 * @param string $original The source string. 469 * @param string $translation The translation. 470 * @param GP_Locale $locale The locale of the translation. 471 * @return string|true True if check is OK, otherwise warning message. 472 */ 473 public function warning_should_end_on_newline( $original, $translation, $locale ) { 474 if ( gp_endswith( $original, "\n" ) && ! gp_endswith( $translation, "\n" ) ) { 475 return __( 'Original and translation should both end on newline.', 'glotpress' ); 476 } 477 478 return true; 479 } 480 481 /** 482 * Checks whether a translation doesn't end on newline. 483 * 484 * @since 1.0.0 485 * @access public 486 * 487 * @param string $original The source string. 488 * @param string $translation The translation. 489 * @param GP_Locale $locale The locale of the translation. 490 * @return string|true True if check is OK, otherwise warning message. 491 */ 492 public function warning_should_not_end_on_newline( $original, $translation, $locale ) { 493 if ( ! gp_endswith( $original, "\n" ) && gp_endswith( $translation, "\n" ) ) { 494 return __( 'Translation should not end on newline.', 'glotpress' ); 495 } 496 497 return true; 498 } 499 500 /** 501 * Adds a warning for changing plain-text URLs. 502 * 503 * This allows for the scheme to change, and for some domains to change to a subdomain. 504 * 505 * @since 3.0.0 506 * @access public 507 * 508 * @param string $original The original string. 509 * @param string $translation The translated string. 510 * @return string|true True if check is OK, otherwise warning message. 511 */ 512 public function warning_mismatching_urls( $original, $translation ) { 513 // Any http/https/schemeless URLs which are not encased in quotation marks 514 // nor contain whitespace and end with a valid URL ending char. 515 $urls_regex = '@(?<![\'"])((https?://|(?<![:\w])//)[^\s<]+[a-z0-9\-_&=#/])(?![\'"])@i'; 516 517 preg_match_all( $urls_regex, $original, $original_urls ); 518 $original_urls = array_unique( $original_urls[0] ); 519 520 preg_match_all( $urls_regex, $translation, $translation_urls ); 521 $translation_urls = array_unique( $translation_urls[0] ); 522 523 $missing_urls = array_diff( $original_urls, $translation_urls ); 524 $added_urls = array_diff( $translation_urls, $original_urls ); 525 if ( ! $missing_urls && ! $added_urls ) { 526 return true; 527 } 528 529 // Check to see if only the scheme (https <=> http) or a trailing slash was changed, discard if so. 530 foreach ( $missing_urls as $key => $missing_url ) { 531 $scheme = parse_url( $missing_url, PHP_URL_SCHEME ); 532 $alternate_scheme = ( 'http' == $scheme ? 'https' : 'http' ); 533 $alternate_scheme_url = preg_replace( "@^$scheme(?=:)@", $alternate_scheme, $missing_url ); 534 535 $alt_urls = array( 536 // Scheme changes. 537 $alternate_scheme_url, 538 539 // Slashed/unslashed changes. 540 ( '/' === substr( $missing_url, -1 ) ? rtrim( $missing_url, '/' ) : "$missing_url/" ), 541 542 // Scheme & Slash changes. 543 ( '/' === substr( $alternate_scheme_url, -1 ) ? rtrim( $alternate_scheme_url, '/' ) : "$alternate_scheme_url/" ), 544 ); 545 546 foreach ( $alt_urls as $alt_url ) { 547 $alternate_index = array_search( $alt_url, $added_urls ); 548 if ( false !== $alternate_index ) { 549 unset( $missing_urls[ $key ], $added_urls[ $alternate_index ] ); 550 } 551 } 552 } 553 554 // Check if just the domain was changed, and if so, if it's to a whitelisted domain. 555 foreach ( $missing_urls as $key => $missing_url ) { 556 $host = parse_url( $missing_url, PHP_URL_HOST ); 557 if ( ! isset( $this->allowed_domain_changes[ $host ] ) ) { 558 continue; 559 } 560 $allowed_host_regex = $this->allowed_domain_changes[ $host ]; 561 562 list( , $missing_url_path ) = explode( $host, $missing_url, 2 ); 563 564 $alternate_host_regex = '!^https?://' . $allowed_host_regex . preg_quote( $missing_url_path, '!' ) . '$!i'; 565 foreach ( $added_urls as $added_index => $added_url ) { 566 if ( preg_match( $alternate_host_regex, $added_url, $match ) ) { 567 unset( $missing_urls[ $key ], $added_urls[ $added_index ] ); 568 } 569 } 570 } 571 572 if ( ! $missing_urls && ! $added_urls ) { 573 return true; 574 } 575 576 $error = ''; 577 if ( $missing_urls ) { 578 $error .= sprintf( 579 /* translators: %s: URLs. */ 580 __( 'The translation appears to be missing the following URLs: %s', 'glotpress' ), 581 implode( ', ', $missing_urls ) . "\n" 582 ); 583 } 584 if ( $added_urls ) { 585 $error .= sprintf( 586 /* translators: %s: URLs. */ 587 __( 'The translation contains the following unexpected URLs: %s', 'glotpress' ), 588 implode( ', ', $added_urls ) . "\n" 589 ); 590 } 591 592 return trim( $error ); 593 } 594 595 /** 596 * Adds a warning for adding unexpected percent signs in a sprintf-like string. 597 * 598 * This is to catch translations for originals like this: 599 * - Original: `<a href="%s">100 percent</a>` 600 * - Submitted translation: `<a href="%s">100%</a>` 601 * - Proper translation: `<a href="%s">100%%</a>` 602 * 603 * @since 3.0.0 604 * @access public 605 * 606 * @param string $original The original string. 607 * @param string $translation The translated string. 608 * @return bool|string 609 */ 610 public function warning_unexpected_sprintf_token( $original, $translation ) { 611 $unexpected_tokens = array(); 612 $is_sprintf = preg_match( '!%((\d+\$(?:\d+)?)?[bcdefgosuxl])\b!i', $original ); 613 614 // Find any percents that are not valid or escaped. 615 if ( $is_sprintf ) { 616 // Negative/Positive lookahead not used to allow the warning to include the context around the % sign. 617 preg_match_all( '/(?P<context>[^\s%]*)%((\d+\$(?:\d+)?)?(?P<char>.))/i', $translation, $m ); 618 foreach ( $m['char'] as $i => $char ) { 619 // % is included for escaped %%. 620 if ( false === strpos( 'bcdefgosux%l.', $char ) ) { 621 $unexpected_tokens[] = $m[0][ $i ]; 622 } 623 } 624 } 625 626 if ( $unexpected_tokens ) { 627 return sprintf( 628 /* translators: %s: Placeholders. */ 629 __( 'The translation contains the following unexpected placeholders: %s', 'glotpress' ), 630 implode( ', ', $unexpected_tokens ) 631 ); 632 } 633 634 return true; 635 } 636 637 /** 638 * Registers all methods starting with `warning_` as built-in warnings. 639 * 640 * @param GP_Translation_Warnings $translation_warnings Instance of GP_Translation_Warnings. 641 */ 642 public function add_all( $translation_warnings ) { 643 $warnings = array_filter( 644 get_class_methods( get_class( $this ) ), 645 function ( $key ) { 646 return gp_startswith( $key, 'warning_' ); 647 } 648 ); 649 650 $warnings = array_fill_keys( $warnings, $this ); 651 652 foreach ( $warnings as $warning => $class ) { 653 $translation_warnings->add( str_replace( 'warning_', '', $warning ), array( $class, $warning ) ); 654 } 655 } 656 657 /** 658 * Adds a warning for changing placeholders. 659 * 660 * This only supports placeholders in the format of '###[A-Za-z_-]+###'. 661 * 662 * @todo Check that the number of each type of placeholders are the same in the original and in the translation 663 * 664 * @since 3.0.0 665 * @access public 666 * 667 * @param string $original The original string. 668 * @param string $translation The translated string. 669 * @return string|true 670 */ 671 public function warning_named_placeholders( string $original, string $translation ) { 672 $placeholder_regex = '@(###[A-Za-z_-]+###)@'; 673 674 preg_match_all( $placeholder_regex, $original, $original_placeholders ); 675 $original_placeholders = array_unique( $original_placeholders[0] ); 676 677 preg_match_all( $placeholder_regex, $translation, $translation_placeholders ); 678 $translation_placeholders = array_unique( $translation_placeholders[0] ); 679 680 $missing_placeholders = array_diff( $original_placeholders, $translation_placeholders ); 681 $added_placeholders = array_diff( $translation_placeholders, $original_placeholders ); 682 if ( ! $missing_placeholders && ! $added_placeholders ) { 683 return true; 684 } 685 686 $error = ''; 687 if ( $missing_placeholders ) { 688 $error .= sprintf( 689 /* translators: %s: Placeholders. */ 690 __( 'The translation appears to be missing the following placeholders: %s', 'glotpress' ), 691 implode( ', ', $missing_placeholders ) . "\n" 692 ); 693 } 694 if ( $added_placeholders ) { 695 $error .= sprintf( 696 /* translators: %s: Placeholders. */ 697 __( 'The translation contains the following unexpected placeholders: %s', 'glotpress' ), 698 implode( ', ', $added_placeholders ) 699 ); 700 } 701 702 return trim( $error ); 703 } 704 705 /** 706 * Returns the values from the href and the src 707 * 708 * @since 3.0.0 709 * @access private 710 * 711 * @param array $content The original array. 712 * @return array 713 */ 714 private function get_values_from_href_src( array $content ): array { 715 preg_match_all( '/<a[^>]+href=([\'"])(?<href>.+?)\1[^>]*>/i', implode( ' ', $content ), $href_values ); 716 preg_match_all( '/<[^>]+src=([\'"])(?<src>.+?)\1[^>]*>/i', implode( ' ', $content ), $src_values ); 717 return array_merge( $href_values['href'], $src_values['src'] ); 718 } 719 720 /** 721 * Checks if the HTML tags are in correct order 722 * 723 * Warns about HTML tags translations in incorrect order. For example: 724 * - Original: <a></a> 725 * - Translation: </a><a> 726 * 727 * @param array $original_parts The original HTML tags. 728 * @param array $translation_parts The translation HTML tags. 729 * @return string|true True if check is OK, otherwise warning message. 730 */ 731 private function check_valid_html( array $original_parts, array $translation_parts ) { 732 if ( empty( $original_parts ) ) { 733 return true; 734 } 735 if ( $original_parts === $translation_parts ) { 736 return true; 737 } 738 739 libxml_clear_errors(); 740 libxml_use_internal_errors( true ); 741 $original = new DOMDocument(); 742 $original->loadHTML( implode( '', $original_parts ) ); 743 // If the original parts are not well-formed, don't continue the translation check. 744 $errors = libxml_get_errors(); 745 if ( ! empty( $errors ) ) { 746 return true; 747 } 748 749 $translation = new DOMDocument(); 750 $translation->loadHTML( implode( '', $translation_parts ) ); 751 $errors = libxml_get_errors(); 752 if ( ! empty( $errors ) ) { 753 $message = array(); 754 foreach ( $errors as $error ) { 755 $message[] = trim( $error->message ); 756 } 757 return sprintf( 758 /* translators: %s: HTML tags. */ 759 __( 'The translation contains incorrect HTML tags: %s', 'glotpress' ), 760 implode( ', ', $message ) 761 ); 762 } 763 return true; 764 } 765 766 /** 767 * Checks whether links that are not URL or placeholders are equal or not 768 * 769 * @since 3.0.0 770 * @access private 771 * 772 * @param string $original_links The original links. 773 * @param string $translation_links The translated links. 774 * @return array|true True if check is OK, otherwise warning message. 775 */ 776 private function links_without_url_and_placeholders_are_equal( string $original_links, string $translation_links ) { 777 $urls_regex = '@(?<![\'"])((https?://|(?<![:\w])//)[^\s<]+[a-z0-9\-_&=#/])(?![\'"])@i'; 778 $placeholder_regex = '!%((\d+\$(?:\d+)?)?[bcdefgosux])!i'; 779 780 // Remove the URLs. 781 preg_match_all( $urls_regex, $original_links, $original_urls ); 782 $original_urls = array_unique( $original_urls[0] ); 783 $original_links_without_url = array_diff( explode( "\n", $original_links ), $original_urls ); 784 preg_match_all( $urls_regex, $translation_links, $translation_urls ); 785 $translation_urls = array_unique( $translation_urls[0] ); 786 $translation_links_without_url = array_diff( explode( "\n", $translation_links ), $translation_urls ); 787 788 // Remove the placeholders. 789 preg_match_all( $placeholder_regex, implode( ' ', $original_links_without_url ), $original_clean_links ); 790 preg_match_all( $placeholder_regex, implode( ' ', $translation_links_without_url ), $translation_clean_links ); 791 $original_clean_links = array_filter( array_diff( $original_links_without_url, $original_clean_links[0] ) ); 792 $translation_clean_links = array_filter( array_diff( $translation_links_without_url, $translation_clean_links[0] ) ); 793 $missing_urls = array_diff( $original_clean_links, $translation_clean_links ); 794 $added_urls = array_diff( $translation_clean_links, $original_clean_links ); 795 796 if ( ! $missing_urls && ! $added_urls ) { 797 return true; 798 } 799 800 $error = array(); 801 if ( $missing_urls ) { 802 $error[] = sprintf( 803 /* translators: %s: URLs. */ 804 __( 'The translation appears to be missing the following links: %s', 'glotpress' ), 805 implode( ', ', $missing_urls ) 806 ); 807 } 808 if ( $added_urls ) { 809 $error[] = sprintf( 810 /* translators: %s: URLs. */ 811 __( 'The translation contains the following unexpected links: %s', 'glotpress' ), 812 implode( ', ', $added_urls ) 813 ); 814 } 815 816 return $error; 817 } 818 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Dec 26 01:01:15 2024 | Cross-referenced by PHPXref 0.7.1 |