[ Index ]

PHP Cross Reference of GlotPress

title

Body

[close]

/gp-includes/ -> warnings.php (source)

   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  }


Generated: Thu Dec 26 01:01:15 2024 Cross-referenced by PHPXref 0.7.1