[ 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       * Checks whether lengths of source and translation differ too much.
 158       *
 159       * @since 1.0.0
 160       * @access public
 161       *
 162       * @param string    $original    The source string.
 163       * @param string    $translation The translation.
 164       * @param GP_Locale $locale      The locale of the translation.
 165       * @return string|true True if check is OK, otherwise warning message.
 166       */
 167  	public function warning_length( $original, $translation, $locale ) {
 168          if ( in_array( $locale->slug, $this->length_exclude_languages, true ) ) {
 169              return true;
 170          }
 171  
 172          if ( gp_startswith( $original, 'number_format_' ) ) {
 173              return true;
 174          }
 175  
 176          $len_src   = mb_strlen( $original );
 177          $len_trans = mb_strlen( $translation );
 178          if (
 179              ! (
 180                  $this->length_lower_bound * $len_src < $len_trans &&
 181                  $len_trans < $this->length_upper_bound * $len_src
 182              ) &&
 183              (
 184                  ! gp_in( '_abbreviation', $original ) &&
 185                  ! gp_in( '_initial', $original ) )
 186          ) {
 187              return __( 'Lengths of source and translation differ too much.', 'glotpress' );
 188          }
 189  
 190          return true;
 191      }
 192  
 193      /**
 194       * Checks whether HTML tags are missing or have been added.
 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_tags( $original, $translation, $locale ) {
 205          $tag_pattern       = '(<[^>]*>)';
 206          $tag_re            = "/$tag_pattern/Us";
 207          $original_parts    = preg_split( $tag_re, $original, - 1, PREG_SPLIT_DELIM_CAPTURE );
 208          $translation_parts = preg_split( $tag_re, $translation, - 1, PREG_SPLIT_DELIM_CAPTURE );
 209  
 210          if ( count( $original_parts ) > count( $translation_parts ) ) {
 211              return __( 'Missing tags from translation.', 'glotpress' );
 212          }
 213          if ( count( $original_parts ) < count( $translation_parts ) ) {
 214              return __( 'Too many tags in translation.', 'glotpress' );
 215          }
 216  
 217          // We allow certain attributes to be different in translations.
 218          $translatable_attributes = array( 'title', 'aria-label' );
 219          $translatable_attr_regex = array();
 220  
 221          foreach ( $translatable_attributes as $key => $attribute ) {
 222              // Translations should never need a quote in a translatable attribute.
 223              $attr_regex_single               = '\s*' . $attribute . '=\'[^\']+\'\s*';
 224              $translatable_attr_regex[ $key ] = '%' . $attr_regex_single . '|' . str_replace( "'", '"', $attr_regex_single ) . '%';
 225          }
 226  
 227          $parts_tags = gp_array_zip( $original_parts, $translation_parts );
 228  
 229          if ( ! empty( $parts_tags ) ) {
 230              foreach ( $parts_tags as $tags ) {
 231                  list( $original_tag, $translation_tag ) = $tags;
 232                  $expected_error_msg                     = "Expected $original_tag, got $translation_tag.";
 233                  $original_is_tag                        = preg_match( "/^$tag_pattern$/", $original_tag );
 234                  $translation_is_tag                     = preg_match( "/^$tag_pattern$/", $translation_tag );
 235  
 236                  if ( $original_is_tag && $translation_is_tag && $original_tag !== $translation_tag ) {
 237                      $original_tag    = preg_replace( $translatable_attr_regex, '', $original_tag );
 238                      $translation_tag = preg_replace( $translatable_attr_regex, '', $translation_tag );
 239                      if ( $original_tag !== $translation_tag ) {
 240                          return $expected_error_msg;
 241                      }
 242                  }
 243              }
 244          }
 245  
 246          return true;
 247      }
 248  
 249      /**
 250       * Checks whether PHP placeholders are missing or have been added.
 251       *
 252       * @since 1.0.0
 253       * @access public
 254       *
 255       * @param string    $original    The source string.
 256       * @param string    $translation The translation.
 257       * @param GP_Locale $locale      The locale of the translation.
 258       * @return string|true True if check is OK, otherwise warning message.
 259       */
 260  	public function warning_placeholders( $original, $translation, $locale ) {
 261          /**
 262           * Filter the regular expression that is used to match placeholders in translations.
 263           *
 264           * @since 1.0.0
 265           *
 266           * @param string $placeholders_re Regular expression pattern without leading or trailing slashes.
 267           */
 268          $placeholders_re = apply_filters( 'gp_warning_placeholders_re', '%(\d+\$(?:\d+)?)?[bcdefgosuxEFGX]' );
 269  
 270          $original_counts    = $this->_placeholders_counts( $original, $placeholders_re );
 271          $translation_counts = $this->_placeholders_counts( $translation, $placeholders_re );
 272          $all_placeholders   = array_unique( array_merge( array_keys( $original_counts ), array_keys( $translation_counts ) ) );
 273          foreach ( $all_placeholders as $placeholder ) {
 274              $original_count    = gp_array_get( $original_counts, $placeholder, 0 );
 275              $translation_count = gp_array_get( $translation_counts, $placeholder, 0 );
 276              if ( $original_count > $translation_count ) {
 277                  return sprintf(
 278                      /* translators: %s: Placeholder. */
 279                      __( 'Missing %s placeholder in translation.', 'glotpress' ),
 280                      $placeholder
 281                  );
 282              }
 283              if ( $original_count < $translation_count ) {
 284                  return sprintf(
 285                      /* translators: %s: Placeholder. */
 286                      __( 'Extra %s placeholder in translation.', 'glotpress' ),
 287                      $placeholder
 288                  );
 289              }
 290          }
 291  
 292          return true;
 293      }
 294  
 295      /**
 296       * Counts the placeholders in a string.
 297       *
 298       * @since 1.0.0
 299       * @access private
 300       *
 301       * @param string $string The string to search.
 302       * @param string $re     Regular expressions to match placeholders.
 303       * @return array An array with counts per placeholder.
 304       */
 305  	private function _placeholders_counts( $string, $re ) {
 306          $counts = array();
 307          preg_match_all( "/$re/", $string, $matches );
 308          foreach ( $matches[0] as $match ) {
 309              $counts[ $match ] = gp_array_get( $counts, $match, 0 ) + 1;
 310          }
 311  
 312          return $counts;
 313      }
 314  
 315      /**
 316       * Checks whether a translation does begin on newline.
 317       *
 318       * @since 1.0.0
 319       * @access public
 320       *
 321       * @param string    $original    The source string.
 322       * @param string    $translation The translation.
 323       * @param GP_Locale $locale      The locale of the translation.
 324       * @return string|true True if check is OK, otherwise warning message.
 325       */
 326  	public function warning_should_begin_on_newline( $original, $translation, $locale ) {
 327          if ( gp_startswith( $original, "\n" ) && ! gp_startswith( $translation, "\n" ) ) {
 328              return __( 'Original and translation should both begin on newline.', 'glotpress' );
 329          }
 330  
 331          return true;
 332      }
 333  
 334      /**
 335       * Checks whether a translation doesn't begin on newline.
 336       *
 337       * @since 1.0.0
 338       * @access public
 339       *
 340       * @param string    $original    The source string.
 341       * @param string    $translation The translation.
 342       * @param GP_Locale $locale      The locale of the translation.
 343       * @return string|true True if check is OK, otherwise warning message.
 344       */
 345  	public function warning_should_not_begin_on_newline( $original, $translation, $locale ) {
 346          if ( ! gp_startswith( $original, "\n" ) && gp_startswith( $translation, "\n" ) ) {
 347              return __( 'Translation should not begin on newline.', 'glotpress' );
 348          }
 349  
 350          return true;
 351      }
 352  
 353      /**
 354       * Checks whether a translation does end on newline.
 355       *
 356       * @since 1.0.0
 357       * @access public
 358       *
 359       * @param string    $original    The source string.
 360       * @param string    $translation The translation.
 361       * @param GP_Locale $locale      The locale of the translation.
 362       * @return string|true True if check is OK, otherwise warning message.
 363       */
 364  	public function warning_should_end_on_newline( $original, $translation, $locale ) {
 365          if ( gp_endswith( $original, "\n" ) && ! gp_endswith( $translation, "\n" ) ) {
 366              return __( 'Original and translation should both end on newline.', 'glotpress' );
 367          }
 368  
 369          return true;
 370      }
 371  
 372      /**
 373       * Checks whether a translation doesn't end on newline.
 374       *
 375       * @since 1.0.0
 376       * @access public
 377       *
 378       * @param string    $original    The source string.
 379       * @param string    $translation The translation.
 380       * @param GP_Locale $locale      The locale of the translation.
 381       * @return string|true True if check is OK, otherwise warning message.
 382       */
 383  	public function warning_should_not_end_on_newline( $original, $translation, $locale ) {
 384          if ( ! gp_endswith( $original, "\n" ) && gp_endswith( $translation, "\n" ) ) {
 385              return __( 'Translation should not end on newline.', 'glotpress' );
 386          }
 387  
 388          return true;
 389      }
 390  
 391      /**
 392       * Registers all methods starting with `warning_` as built-in warnings.
 393       *
 394       * @param GP_Translation_Warnings $translation_warnings Instance of GP_Translation_Warnings.
 395       */
 396  	public function add_all( $translation_warnings ) {
 397          $warnings = array_filter(
 398              get_class_methods( get_class( $this ) ),
 399              function ( $key ) {
 400                  return gp_startswith( $key, 'warning_' );
 401              }
 402          );
 403  
 404          foreach ( $warnings as $warning ) {
 405              $translation_warnings->add( str_replace( 'warning_', '', $warning ), array( $this, $warning ) );
 406          }
 407      }
 408  }


Generated: Wed Jul 8 01:01:56 2020 Cross-referenced by PHPXref 0.7.1