[ 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 array
  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 array     $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( 'singular' => false, 'plural' => false );
  85              if ( null !== $plural ) {
  86                  $numbers_for_index = $locale->numbers_for_index( $translation_index );
  87                  if ( 1 === $locale->nplurals ) {
  88                      $skip['singular'] = true;
  89                  } elseif ( in_array( 1, $numbers_for_index, true ) ) {
  90                      $skip['plural'] = true;
  91                  } else {
  92                      $skip['singular'] = true;
  93                  }
  94              }
  95  
  96              foreach ( $this->callbacks as $callback_id => $callback ) {
  97                  if ( ! $skip['singular'] ) {
  98                      $singular_test = call_user_func( $callback, $singular, $translation, $locale );
  99                      if ( true !== $singular_test ) {
 100                          $problems[ $translation_index ][ $callback_id ] = $singular_test;
 101                      }
 102                  }
 103                  if ( ! is_null( $plural ) && ! $skip['plural'] ) {
 104                      $plural_test = call_user_func( $callback, $plural, $translation, $locale );
 105                      if ( true !== $plural_test ) {
 106                          $problems[ $translation_index ][ $callback_id ] = $plural_test;
 107                      }
 108                  }
 109              }
 110          }
 111  
 112          return empty( $problems ) ? null : $problems;
 113      }
 114  }
 115  
 116  /**
 117   * Class used to register built-in translation warnings.
 118   *
 119   * @since 1.0.0
 120   */
 121  class GP_Builtin_Translation_Warnings {
 122  
 123      /**
 124       * Lower bound for length checks.
 125       *
 126       * @since 1.0.0
 127       * @access public
 128       *
 129       * @var float
 130       */
 131      public $length_lower_bound = 0.2;
 132  
 133      /**
 134       * Upper bound for length checks.
 135       *
 136       * @since 1.0.0
 137       * @access public
 138       *
 139       * @var float
 140       */
 141      public $length_upper_bound = 5.0;
 142  
 143      /**
 144       * List of locales which are excluded from length checks.
 145       *
 146       * @since 1.0.0
 147       * @access public
 148       *
 149       * @var array
 150       */
 151      public $length_exclude_languages = array( 'art-xemoji', 'ja', 'ko', 'zh', 'zh-hk', 'zh-cn', 'zh-sg', 'zh-tw' );
 152  
 153      /**
 154       * Checks whether lengths of source and translation differ too much.
 155       *
 156       * @since 1.0.0
 157       * @access public
 158       *
 159       * @param string    $original    The source string.
 160       * @param string    $translation The translation.
 161       * @param GP_Locale $locale      The locale of the translation.
 162       * @return string|true True if check is OK, otherwise warning message.
 163       */
 164  	public function warning_length( $original, $translation, $locale ) {
 165          if ( in_array( $locale->slug, $this->length_exclude_languages, true ) ) {
 166              return true;
 167          }
 168  
 169          if ( gp_startswith( $original, 'number_format_' ) ) {
 170              return true;
 171          }
 172  
 173          $len_src   = gp_strlen( $original );
 174          $len_trans = gp_strlen( $translation );
 175          if (
 176              ! (
 177                  $this->length_lower_bound * $len_src < $len_trans &&
 178                  $len_trans < $this->length_upper_bound * $len_src
 179              ) &&
 180              (
 181                  ! gp_in( '_abbreviation', $original ) &&
 182                  ! gp_in( '_initial', $original ) )
 183          ) {
 184              return __( 'Lengths of source and translation differ too much.', 'glotpress' );
 185          }
 186  
 187          return true;
 188      }
 189  
 190      /**
 191       * Checks whether HTML tags are missing or have been added.
 192       *
 193       * @since 1.0.0
 194       * @access public
 195       *
 196       * @param string    $original    The source string.
 197       * @param string    $translation The translation.
 198       * @param GP_Locale $locale      The locale of the translation.
 199       * @return string|true True if check is OK, otherwise warning message.
 200       */
 201  	public function warning_tags( $original, $translation, $locale ) {
 202          $tag_pattern       = '(<[^>]*>)';
 203          $tag_re            = "/$tag_pattern/Us";
 204          $original_parts    = preg_split( $tag_re, $original, - 1, PREG_SPLIT_DELIM_CAPTURE );
 205          $translation_parts = preg_split( $tag_re, $translation, - 1, PREG_SPLIT_DELIM_CAPTURE );
 206  
 207          if ( count( $original_parts ) > count( $translation_parts ) ) {
 208              return __( 'Missing tags from translation.', 'glotpress' );
 209          }
 210          if ( count( $original_parts ) < count( $translation_parts ) ) {
 211              return __( 'Too many tags in translation.', 'glotpress' );
 212          }
 213  
 214          // We allow certain attributes to be different in translations.
 215          $translatable_attributes = array( 'title', 'aria-label' );
 216          $translatable_attr_regex = array();
 217  
 218          foreach ( $translatable_attributes as $key => $attribute ) {
 219              // Translations should never need a quote in a translatable attribute.
 220              $attr_regex_single               = '\s*' . $attribute . '=\'[^\']+\'\s*';
 221              $translatable_attr_regex[ $key ] = '%' . $attr_regex_single . '|' . str_replace( "'", '"', $attr_regex_single ) . '%';
 222          }
 223  
 224          $parts_tags = gp_array_zip( $original_parts, $translation_parts );
 225  
 226          if ( ! empty( $parts_tags ) ) {
 227              foreach ( $parts_tags as $tags ) {
 228                  list( $original_tag, $translation_tag ) = $tags;
 229                  $expected_error_msg = "Expected $original_tag, got $translation_tag.";
 230                  $original_is_tag    = preg_match( "/^$tag_pattern$/", $original_tag );
 231                  $translation_is_tag = preg_match( "/^$tag_pattern$/", $translation_tag );
 232  
 233                  if ( $original_is_tag && $translation_is_tag && $original_tag !== $translation_tag ) {
 234                      $original_tag    = preg_replace( $translatable_attr_regex, '', $original_tag );
 235                      $translation_tag = preg_replace( $translatable_attr_regex, '', $translation_tag );
 236                      if ( $original_tag !== $translation_tag ) {
 237                          return $expected_error_msg;
 238                      }
 239                  }
 240              }
 241          }
 242  
 243          return true;
 244      }
 245  
 246      /**
 247       * Checks whether PHP placeholders are missing or have been added.
 248       *
 249       * @since 1.0.0
 250       * @access public
 251       *
 252       * @param string    $original    The source string.
 253       * @param string    $translation The translation.
 254       * @param GP_Locale $locale      The locale of the translation.
 255       * @return string|true True if check is OK, otherwise warning message.
 256       */
 257  	public function warning_placeholders( $original, $translation, $locale ) {
 258          /**
 259           * Filter the regular expression that is used to match placeholders in translations.
 260           *
 261           * @since 1.0.0
 262           *
 263           * @param string $placeholders_re Regular expression pattern without leading or trailing slashes.
 264           */
 265          $placeholders_re = apply_filters( 'gp_warning_placeholders_re', '%(\d+\$(?:\d+)?)?[bcdefgosuxEFGX]' );
 266  
 267          $original_counts    = $this->_placeholders_counts( $original, $placeholders_re );
 268          $translation_counts = $this->_placeholders_counts( $translation, $placeholders_re );
 269          $all_placeholders   = array_unique( array_merge( array_keys( $original_counts ), array_keys( $translation_counts ) ) );
 270          foreach ( $all_placeholders as $placeholder ) {
 271              $original_count    = gp_array_get( $original_counts, $placeholder, 0 );
 272              $translation_count = gp_array_get( $translation_counts, $placeholder, 0 );
 273              if ( $original_count > $translation_count ) {
 274                  return sprintf( __( 'Missing %s placeholder in translation.', 'glotpress' ), $placeholder );
 275              }
 276              if ( $original_count < $translation_count ) {
 277                  return sprintf( __( 'Extra %s placeholder in translation.', 'glotpress' ), $placeholder );
 278              }
 279          }
 280  
 281          return true;
 282      }
 283  
 284      /**
 285       * Counts the placeholders in a string.
 286       *
 287       * @since 1.0.0
 288       * @access private
 289       *
 290       * @param string $string The string to search.
 291       * @param string $re     Regular expressions to match placeholders.
 292       * @return array An array with counts per placeholder.
 293       */
 294  	private function _placeholders_counts( $string, $re ) {
 295          $counts = array();
 296          preg_match_all( "/$re/", $string, $matches );
 297          foreach ( $matches[0] as $match ) {
 298              $counts[ $match ] = gp_array_get( $counts, $match, 0 ) + 1;
 299          }
 300  
 301          return $counts;
 302      }
 303  
 304      /**
 305       * Checks whether a translation does begin on newline.
 306       *
 307       * @since 1.0.0
 308       * @access public
 309       *
 310       * @param string    $original    The source string.
 311       * @param string    $translation The translation.
 312       * @param GP_Locale $locale      The locale of the translation.
 313       * @return string|true True if check is OK, otherwise warning message.
 314       */
 315  	public function warning_should_begin_on_newline( $original, $translation, $locale ) {
 316          if ( gp_startswith( $original, "\n" ) && ! gp_startswith( $translation, "\n" ) ) {
 317              return __( 'Original and translation should both begin on newline.', 'glotpress' );
 318          }
 319  
 320          return true;
 321      }
 322  
 323      /**
 324       * Checks whether a translation doesn't begin on newline.
 325       *
 326       * @since 1.0.0
 327       * @access public
 328       *
 329       * @param string    $original    The source string.
 330       * @param string    $translation The translation.
 331       * @param GP_Locale $locale      The locale of the translation.
 332       * @return string|true True if check is OK, otherwise warning message.
 333       */
 334  	public function warning_should_not_begin_on_newline( $original, $translation, $locale ) {
 335          if ( ! gp_startswith( $original, "\n" ) && gp_startswith( $translation, "\n" ) ) {
 336              return __( 'Translation should not begin on newline.', 'glotpress' );
 337          }
 338  
 339          return true;
 340      }
 341  
 342      /**
 343       * Checks whether a translation does end on newline.
 344       *
 345       * @since 1.0.0
 346       * @access public
 347       *
 348       * @param string    $original    The source string.
 349       * @param string    $translation The translation.
 350       * @param GP_Locale $locale      The locale of the translation.
 351       * @return string|true True if check is OK, otherwise warning message.
 352       */
 353  	public function warning_should_end_on_newline( $original, $translation, $locale ) {
 354          if ( gp_endswith( $original, "\n" ) && ! gp_endswith( $translation, "\n" ) ) {
 355              return __( 'Original and translation should both end on newline.', 'glotpress' );
 356          }
 357  
 358          return true;
 359      }
 360  
 361      /**
 362       * Checks whether a translation doesn't end on newline.
 363       *
 364       * @since 1.0.0
 365       * @access public
 366       *
 367       * @param string    $original    The source string.
 368       * @param string    $translation The translation.
 369       * @param GP_Locale $locale      The locale of the translation.
 370       * @return string|true True if check is OK, otherwise warning message.
 371       */
 372  	public function warning_should_not_end_on_newline( $original, $translation, $locale ) {
 373          if ( ! gp_endswith( $original, "\n" ) && gp_endswith( $translation, "\n" ) ) {
 374              return __( 'Translation should not end on newline.', 'glotpress' );
 375          }
 376  
 377          return true;
 378      }
 379  
 380      /**
 381       * Registers all methods starting with `warning_` as built-in warnings.
 382       *
 383       * @param GP_Translation_Warnings $translation_warnings Instance of GP_Translation_Warnings.
 384       */
 385  	public function add_all( $translation_warnings ) {
 386          $warnings = array_filter( get_class_methods( get_class( $this ) ), function ( $key ) {
 387              return gp_startswith( $key, 'warning_' );
 388          } );
 389  
 390          foreach ( $warnings as $warning ) {
 391              $translation_warnings->add( str_replace( 'warning_', '', $warning ), array( $this, $warning ) );
 392          }
 393      }
 394  }


Generated: Thu Dec 12 01:01:56 2019 Cross-referenced by PHPXref 0.7.1