[ 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       * @since 1.0.0
 356       * @access public
 357       *
 358       * @param string    $original    The source string.
 359       * @param string    $translation The translation.
 360       * @param GP_Locale $locale      The locale of the translation.
 361       * @return string|true True if check is OK, otherwise warning message.
 362       */
 363  	public function warning_placeholders( $original, $translation, $locale ) {
 364          /**
 365           * Filter the regular expression that is used to match placeholders in translations.
 366           *
 367           * @since 1.0.0
 368           *
 369           * @param string $placeholders_re Regular expression pattern without leading or trailing slashes.
 370           */
 371          $placeholders_re = apply_filters( 'gp_warning_placeholders_re', '(?<!%)%(\d+\$(?:\d+)?)?[bcdefgosuxEFGX%l]' );
 372  
 373          $original_counts    = $this->_placeholders_counts( $original, $placeholders_re );
 374          $translation_counts = $this->_placeholders_counts( $translation, $placeholders_re );
 375          $all_placeholders   = array_unique( array_merge( array_keys( $original_counts ), array_keys( $translation_counts ) ) );
 376          foreach ( $all_placeholders as $placeholder ) {
 377              $original_count    = gp_array_get( $original_counts, $placeholder, 0 );
 378              $translation_count = gp_array_get( $translation_counts, $placeholder, 0 );
 379              if ( $original_count > $translation_count ) {
 380                  return sprintf(
 381                      /* translators: %s: Placeholder. */
 382                      __( 'Missing %s placeholder in translation.', 'glotpress' ),
 383                      $placeholder
 384                  );
 385              }
 386              if ( $original_count < $translation_count ) {
 387                  return sprintf(
 388                      /* translators: %s: Placeholder. */
 389                      __( 'Extra %s placeholder in translation.', 'glotpress' ),
 390                      $placeholder
 391                  );
 392              }
 393          }
 394  
 395          return true;
 396      }
 397  
 398      /**
 399       * Counts the placeholders in a string.
 400       *
 401       * @since 1.0.0
 402       * @access private
 403       *
 404       * @param string $string The string to search.
 405       * @param string $re     Regular expressions to match placeholders.
 406       * @return array An array with counts per placeholder.
 407       */
 408  	private function _placeholders_counts( $string, $re ) {
 409          $counts = array();
 410          preg_match_all( "/$re/", $string, $matches );
 411          foreach ( $matches[0] as $match ) {
 412              $counts[ $match ] = gp_array_get( $counts, $match, 0 ) + 1;
 413          }
 414  
 415          return $counts;
 416      }
 417  
 418      /**
 419       * Checks whether a translation does begin on newline.
 420       *
 421       * @since 1.0.0
 422       * @access public
 423       *
 424       * @param string    $original    The source string.
 425       * @param string    $translation The translation.
 426       * @param GP_Locale $locale      The locale of the translation.
 427       * @return string|true True if check is OK, otherwise warning message.
 428       */
 429  	public function warning_should_begin_on_newline( $original, $translation, $locale ) {
 430          if ( gp_startswith( $original, "\n" ) && ! gp_startswith( $translation, "\n" ) ) {
 431              return __( 'Original and translation should both begin on newline.', 'glotpress' );
 432          }
 433  
 434          return true;
 435      }
 436  
 437      /**
 438       * Checks whether a translation doesn't begin on newline.
 439       *
 440       * @since 1.0.0
 441       * @access public
 442       *
 443       * @param string    $original    The source string.
 444       * @param string    $translation The translation.
 445       * @param GP_Locale $locale      The locale of the translation.
 446       * @return string|true True if check is OK, otherwise warning message.
 447       */
 448  	public function warning_should_not_begin_on_newline( $original, $translation, $locale ) {
 449          if ( ! gp_startswith( $original, "\n" ) && gp_startswith( $translation, "\n" ) ) {
 450              return __( 'Translation should not begin on newline.', 'glotpress' );
 451          }
 452  
 453          return true;
 454      }
 455  
 456      /**
 457       * Checks whether a translation does end on newline.
 458       *
 459       * @since 1.0.0
 460       * @access public
 461       *
 462       * @param string    $original    The source string.
 463       * @param string    $translation The translation.
 464       * @param GP_Locale $locale      The locale of the translation.
 465       * @return string|true True if check is OK, otherwise warning message.
 466       */
 467  	public function warning_should_end_on_newline( $original, $translation, $locale ) {
 468          if ( gp_endswith( $original, "\n" ) && ! gp_endswith( $translation, "\n" ) ) {
 469              return __( 'Original and translation should both end on newline.', 'glotpress' );
 470          }
 471  
 472          return true;
 473      }
 474  
 475      /**
 476       * Checks whether a translation doesn't end on newline.
 477       *
 478       * @since 1.0.0
 479       * @access public
 480       *
 481       * @param string    $original    The source string.
 482       * @param string    $translation The translation.
 483       * @param GP_Locale $locale      The locale of the translation.
 484       * @return string|true True if check is OK, otherwise warning message.
 485       */
 486  	public function warning_should_not_end_on_newline( $original, $translation, $locale ) {
 487          if ( ! gp_endswith( $original, "\n" ) && gp_endswith( $translation, "\n" ) ) {
 488              return __( 'Translation should not end on newline.', 'glotpress' );
 489          }
 490  
 491          return true;
 492      }
 493  
 494      /**
 495       * Adds a warning for changing plain-text URLs.
 496       *
 497       * This allows for the scheme to change, and for some domains to change to a subdomain.
 498       *
 499       * @since 3.0.0
 500       * @access public
 501       *
 502       * @param string $original    The original string.
 503       * @param string $translation The translated string.
 504       * @return string|true True if check is OK, otherwise warning message.
 505       */
 506  	public function warning_mismatching_urls( $original, $translation ) {
 507          // Any http/https/schemeless URLs which are not encased in quotation marks
 508          // nor contain whitespace and end with a valid URL ending char.
 509          $urls_regex = '@(?<![\'"])((https?://|(?<![:\w])//)[^\s<]+[a-z0-9\-_&=#/])(?![\'"])@i';
 510  
 511          preg_match_all( $urls_regex, $original, $original_urls );
 512          $original_urls = array_unique( $original_urls[0] );
 513  
 514          preg_match_all( $urls_regex, $translation, $translation_urls );
 515          $translation_urls = array_unique( $translation_urls[0] );
 516  
 517          $missing_urls = array_diff( $original_urls, $translation_urls );
 518          $added_urls   = array_diff( $translation_urls, $original_urls );
 519          if ( ! $missing_urls && ! $added_urls ) {
 520              return true;
 521          }
 522  
 523          // Check to see if only the scheme (https <=> http) or a trailing slash was changed, discard if so.
 524          foreach ( $missing_urls as $key => $missing_url ) {
 525              $scheme               = parse_url( $missing_url, PHP_URL_SCHEME );
 526              $alternate_scheme     = ( 'http' == $scheme ? 'https' : 'http' );
 527              $alternate_scheme_url = preg_replace( "@^$scheme(?=:)@", $alternate_scheme, $missing_url );
 528  
 529              $alt_urls = array(
 530                  // Scheme changes.
 531                  $alternate_scheme_url,
 532  
 533                  // Slashed/unslashed changes.
 534                  ( '/' === substr( $missing_url, -1 ) ? rtrim( $missing_url, '/' ) : "$missing_url/" ),
 535  
 536                  // Scheme & Slash changes.
 537                  ( '/' === substr( $alternate_scheme_url, -1 ) ? rtrim( $alternate_scheme_url, '/' ) : "$alternate_scheme_url/" ),
 538              );
 539  
 540              foreach ( $alt_urls as $alt_url ) {
 541                  $alternate_index = array_search( $alt_url, $added_urls );
 542                  if ( false !== $alternate_index ) {
 543                      unset( $missing_urls[ $key ], $added_urls[ $alternate_index ] );
 544                  }
 545              }
 546          }
 547  
 548          // Check if just the domain was changed, and if so, if it's to a whitelisted domain.
 549          foreach ( $missing_urls as $key => $missing_url ) {
 550              $host = parse_url( $missing_url, PHP_URL_HOST );
 551              if ( ! isset( $this->allowed_domain_changes[ $host ] ) ) {
 552                  continue;
 553              }
 554              $allowed_host_regex = $this->allowed_domain_changes[ $host ];
 555  
 556              list( , $missing_url_path ) = explode( $host, $missing_url, 2 );
 557  
 558              $alternate_host_regex = '!^https?://' . $allowed_host_regex . preg_quote( $missing_url_path, '!' ) . '$!i';
 559              foreach ( $added_urls as $added_index => $added_url ) {
 560                  if ( preg_match( $alternate_host_regex, $added_url, $match ) ) {
 561                      unset( $missing_urls[ $key ], $added_urls[ $added_index ] );
 562                  }
 563              }
 564          }
 565  
 566          if ( ! $missing_urls && ! $added_urls ) {
 567              return true;
 568          }
 569  
 570          $error = '';
 571          if ( $missing_urls ) {
 572              $error .= sprintf(
 573                  /* translators: %s: URLs. */
 574                  __( 'The translation appears to be missing the following URLs: %s', 'glotpress' ),
 575                  implode( ', ', $missing_urls ) . "\n"
 576              );
 577          }
 578          if ( $added_urls ) {
 579              $error .= sprintf(
 580                  /* translators: %s: URLs. */
 581                  __( 'The translation contains the following unexpected URLs: %s', 'glotpress' ),
 582                  implode( ', ', $added_urls ) . "\n"
 583              );
 584          }
 585  
 586          return trim( $error );
 587      }
 588  
 589      /**
 590       * Adds a warning for adding unexpected percent signs in a sprintf-like string.
 591       *
 592       * This is to catch translations for originals like this:
 593       *  - Original: `<a href="%s">100 percent</a>`
 594       *  - Submitted translation: `<a href="%s">100%</a>`
 595       *  - Proper translation: `<a href="%s">100%%</a>`
 596       *
 597       * @since 3.0.0
 598       * @access public
 599       *
 600       * @param string $original    The original string.
 601       * @param string $translation The translated string.
 602       * @return bool|string
 603       */
 604  	public function warning_unexpected_sprintf_token( $original, $translation ) {
 605          $unexpected_tokens = array();
 606          $is_sprintf        = preg_match( '!%((\d+\$(?:\d+)?)?[bcdefgosuxl])\b!i', $original );
 607  
 608          // Find any percents that are not valid or escaped.
 609          if ( $is_sprintf ) {
 610              // Negative/Positive lookahead not used to allow the warning to include the context around the % sign.
 611              preg_match_all( '/(?P<context>[^\s%]*)%((\d+\$(?:\d+)?)?(?P<char>.))/i', $translation, $m );
 612              foreach ( $m['char'] as $i => $char ) {
 613                  // % is included for escaped %%.
 614                  if ( false === strpos( 'bcdefgosux%l', $char ) ) {
 615                      $unexpected_tokens[] = $m[0][ $i ];
 616                  }
 617              }
 618          }
 619  
 620          if ( $unexpected_tokens ) {
 621              return sprintf(
 622                  /* translators: %s: Placeholders. */
 623                  __( 'The translation contains the following unexpected placeholders: %s', 'glotpress' ),
 624                  implode( ', ', $unexpected_tokens )
 625              );
 626          }
 627  
 628          return true;
 629      }
 630  
 631      /**
 632       * Registers all methods starting with `warning_` as built-in warnings.
 633       *
 634       * @param GP_Translation_Warnings $translation_warnings Instance of GP_Translation_Warnings.
 635       */
 636  	public function add_all( $translation_warnings ) {
 637          $warnings = array_filter(
 638              get_class_methods( get_class( $this ) ),
 639              function ( $key ) {
 640                  return gp_startswith( $key, 'warning_' );
 641              }
 642          );
 643  
 644          $warnings = array_fill_keys( $warnings, $this );
 645  
 646          foreach ( $warnings as $warning => $class ) {
 647              $translation_warnings->add( str_replace( 'warning_', '', $warning ), array( $class, $warning ) );
 648          }
 649      }
 650  
 651      /**
 652       * Adds a warning for changing placeholders.
 653       *
 654       * This only supports placeholders in the format of '###[A-Z_]+###'.
 655       *
 656       * @todo Check that the number of each type of placeholders are the same in the original and in the translation
 657       *
 658       * @since 3.0.0
 659       * @access public
 660       *
 661       * @param string $original    The original string.
 662       * @param string $translation The translated string.
 663       * @return string|true
 664       */
 665  	public function warning_named_placeholders( string $original, string $translation ) {
 666          $placeholder_regex = '@(###[A-Z_]+###)@';
 667  
 668          preg_match_all( $placeholder_regex, $original, $original_placeholders );
 669          $original_placeholders = array_unique( $original_placeholders[0] );
 670  
 671          preg_match_all( $placeholder_regex, $translation, $translation_placeholders );
 672          $translation_placeholders = array_unique( $translation_placeholders[0] );
 673  
 674          $missing_placeholders = array_diff( $original_placeholders, $translation_placeholders );
 675          $added_placeholders   = array_diff( $translation_placeholders, $original_placeholders );
 676          if ( ! $missing_placeholders && ! $added_placeholders ) {
 677              return true;
 678          }
 679  
 680          $error = '';
 681          if ( $missing_placeholders ) {
 682              $error .= sprintf(
 683                  /* translators: %s: Placeholders. */
 684                  __( 'The translation appears to be missing the following placeholders: %s', 'glotpress' ),
 685                  implode( ', ', $missing_placeholders ) . "\n"
 686              );
 687          }
 688          if ( $added_placeholders ) {
 689              $error .= sprintf(
 690                  /* translators: %s: Placeholders. */
 691                  __( 'The translation contains the following unexpected placeholders: %s', 'glotpress' ),
 692                  implode( ', ', $added_placeholders )
 693              );
 694          }
 695  
 696          return trim( $error );
 697      }
 698  
 699      /**
 700       * Returns the values from the href and the src
 701       *
 702       * @since 3.0.0
 703       * @access private
 704       *
 705       * @param array $content The original array.
 706       * @return array
 707       */
 708  	private function get_values_from_href_src( array $content ): array {
 709          preg_match_all( '/<a[^>]+href=([\'"])(?<href>.+?)\1[^>]*>/i', implode( ' ', $content ), $href_values );
 710          preg_match_all( '/<[^>]+src=([\'"])(?<src>.+?)\1[^>]*>/i', implode( ' ', $content ), $src_values );
 711          return array_merge( $href_values['href'], $src_values['src'] );
 712      }
 713  
 714      /**
 715       * Checks if the HTML tags are in correct order
 716       *
 717       * Warns about HTML tags translations in incorrect order. For example:
 718       * - Original: <a></a>
 719       * - Translation: </a><a>
 720       *
 721       * @param array $original_parts     The original HTML tags.
 722       * @param array $translation_parts  The translation HTML tags.
 723       * @return string|true True if check is OK, otherwise warning message.
 724       */
 725  	private function check_valid_html( array $original_parts, array $translation_parts ) {
 726          if ( empty( $original_parts ) ) {
 727              return true;
 728          }
 729          if ( $original_parts === $translation_parts ) {
 730              return true;
 731          }
 732  
 733          libxml_clear_errors();
 734          libxml_use_internal_errors( true );
 735          $original = new DOMDocument();
 736          $original->loadHTML( implode( '', $original_parts ) );
 737          // If the original parts are not well-formed, don't continue the translation check.
 738          $errors = libxml_get_errors();
 739          if ( ! empty( $errors ) ) {
 740              return true;
 741          }
 742  
 743          $translation = new DOMDocument();
 744          $translation->loadHTML( implode( '', $translation_parts ) );
 745          $errors = libxml_get_errors();
 746          if ( ! empty( $errors ) ) {
 747              $message = array();
 748              foreach ( $errors as $error ) {
 749                  $message[] = trim( $error->message );
 750              }
 751              return sprintf(
 752                  /* translators: %s: HTML tags. */
 753                  __( 'The translation contains incorrect HTML tags: %s', 'glotpress' ),
 754                  implode( ', ', $message )
 755              );
 756          }
 757          return true;
 758      }
 759  
 760      /**
 761       * Checks whether links that are not URL or placeholders are equal or not
 762       *
 763       * @since 3.0.0
 764       * @access private
 765       *
 766       * @param string $original_links    The original links.
 767       * @param string $translation_links The translated links.
 768       * @return  array|true True if check is OK, otherwise warning message.
 769       */
 770  	private function links_without_url_and_placeholders_are_equal( string $original_links, string $translation_links ) {
 771          $urls_regex        = '@(?<![\'"])((https?://|(?<![:\w])//)[^\s<]+[a-z0-9\-_&=#/])(?![\'"])@i';
 772          $placeholder_regex = '!%((\d+\$(?:\d+)?)?[bcdefgosux])!i';
 773  
 774          // Remove the URLs.
 775          preg_match_all( $urls_regex, $original_links, $original_urls );
 776          $original_urls              = array_unique( $original_urls[0] );
 777          $original_links_without_url = array_diff( explode( "\n", $original_links ), $original_urls );
 778          preg_match_all( $urls_regex, $translation_links, $translation_urls );
 779          $translation_urls              = array_unique( $translation_urls[0] );
 780          $translation_links_without_url = array_diff( explode( "\n", $translation_links ), $translation_urls );
 781  
 782          // Remove the placeholders.
 783          preg_match_all( $placeholder_regex, implode( ' ', $original_links_without_url ), $original_clean_links );
 784          preg_match_all( $placeholder_regex, implode( ' ', $translation_links_without_url ), $translation_clean_links );
 785          $original_clean_links    = array_filter( array_diff( $original_links_without_url, $original_clean_links[0] ) );
 786          $translation_clean_links = array_filter( array_diff( $translation_links_without_url, $translation_clean_links[0] ) );
 787          $missing_urls            = array_diff( $original_clean_links, $translation_clean_links );
 788          $added_urls              = array_diff( $translation_clean_links, $original_clean_links );
 789  
 790          if ( ! $missing_urls && ! $added_urls ) {
 791              return true;
 792          }
 793  
 794          $error = array();
 795          if ( $missing_urls ) {
 796              $error[] = sprintf(
 797                  /* translators: %s: URLs. */
 798                  __( 'The translation appears to be missing the following links: %s', 'glotpress' ),
 799                  implode( ', ', $missing_urls )
 800              );
 801          }
 802          if ( $added_urls ) {
 803              $error[] = sprintf(
 804                  /* translators: %s: URLs. */
 805                  __( 'The translation contains the following unexpected links: %s', 'glotpress' ),
 806                  implode( ', ', $added_urls )
 807              );
 808          }
 809  
 810          return $error;
 811      }
 812  }


Generated: Wed Aug 4 01:02:03 2021 Cross-referenced by PHPXref 0.7.1