[ Index ]

PHP Cross Reference of GlotPress

title

Body

[close]

/gp-includes/formats/ -> format-android.php (source)

   1  <?php
   2  /**
   3   * GlotPress Format Android XML class
   4   *
   5   * @since 1.0.0
   6   *
   7   * @package GlotPress
   8   */
   9  
  10  /**
  11   * Format class used to support Android XML file format.
  12   *
  13   * @since 1.0.0
  14   */
  15  class GP_Format_Android extends GP_Format {
  16      /**
  17       * Name of file format, used in file format dropdowns.
  18       *
  19       * @since 1.0.0
  20       *
  21       * @var string
  22       */
  23      public $name = 'Android XML (.xml)';
  24  
  25      /**
  26       * File extension of the file format, used to autodetect formats and when creating the output file names.
  27       *
  28       * @since 1.0.0
  29       *
  30       * @var string
  31       */
  32      public $extension = 'xml';
  33  
  34      /**
  35       * Storage for the export file contents while it is being generated.
  36       *
  37       * @since 1.0.0
  38       *
  39       * @var string
  40       */
  41      public $exported = '';
  42  
  43      /**
  44       * Generates a string the contains the $entries to export in the Android XML file format.
  45       *
  46       * @since 1.0.0
  47       *
  48       * @param GP_Project         $project         The project the strings are being exported for, not used
  49       *                                            in this format but part of the scaffold of the parent object.
  50       * @param GP_Locale          $locale          The locale object the strings are being exported for, not used
  51       *                                            in this format but part of the scaffold of the parent object.
  52       * @param GP_Translation_Set $translation_set The locale object the strings are being
  53       *                                            exported for. not used in this format but part
  54       *                                            of the scaffold of the parent object.
  55       * @param GP_Translation     $entries         The entries to export.
  56       *
  57       * @return string The exported Android XML string.
  58       */
  59  	public function print_exported_file( $project, $locale, $translation_set, $entries ) {
  60          $this->exported = '';
  61          $this->line( '<?xml version="1.0" encoding="utf-8"?>' );
  62  
  63          $this->line( '<!--' );
  64          $this->line( 'Translation-Revision-Date: ' . GP::$translation->last_modified( $translation_set ) . '+0000' );
  65          $this->line( "Plural-Forms: nplurals={$locale->nplurals}; plural={$locale->plural_expression};" );
  66          $this->line( 'Generator: GlotPress/' . GP_VERSION );
  67  
  68          $language_code = $this->get_language_code( $locale );
  69          if ( false !== $language_code ) {
  70              $this->line( 'Language: ' . $language_code );
  71          }
  72  
  73          $this->line( '-->' );
  74  
  75          $this->line( '<resources>' );
  76          $string_array_items = array();
  77  
  78          foreach ( $entries as $entry ) {
  79              if ( preg_match( '/.+\[\d+\]$/', $entry->context ) ) {
  80                  // Array item found.
  81                  $string_array_items[] = $entry;
  82                  continue;
  83              }
  84  
  85              if ( empty( $entry->context ) ) {
  86                  $entry->context = $entry->singular;
  87              }
  88  
  89              $id = preg_replace( '/[^a-zA-Z0-9_]/U', '_', $entry->context );
  90  
  91              $this->line( '<string name="' . $id . '">' . $this->escape( $entry->translations[0] ) . '</string>', 1 );
  92          }
  93  
  94          $this->string_arrays( $string_array_items );
  95  
  96          $this->line( '</resources>' );
  97  
  98          return $this->exported;
  99      }
 100  
 101      /**
 102       * Reads a set of original strings from an Android XML file.
 103       *
 104       * @since 1.0.0
 105       *
 106       * @param string $file_name The name of the uploaded Android XML file.
 107       *
 108       * @return Translations|bool The extracted originals on success, false on failure.
 109       */
 110  	public function read_originals_from_file( $file_name ) {
 111          // Disable the output of errors while processing the XML file.
 112          $errors = libxml_use_internal_errors( true );
 113  
 114          // Get the contents from the temporary file.
 115          $contents = file_get_contents( $file_name );
 116  
 117          /*
 118           * Android strings can use <xliff:g> tags to indicate a part of the string should NOT be translated.
 119           *
 120           * See the "Mark message parts that should not be translated" section of https://developer.android.com/distribute/tools/localization-checklist.html
 121           *
 122           * Unfortunately SimpleXML will parse these as valid XML tags, which we don't want so replace the opening brace with something we can
 123           * re-instate later to process the xliff tags ourselves.
 124          */
 125          $contents = str_ireplace( '<xliff:g', '--xlifftag--xliff:g', $contents );
 126          $contents = str_ireplace( '</xliff:g>', '--xlifftag--/xliff:g>', $contents );
 127  
 128          // Parse the file contents.
 129          $data = simplexml_load_string( $contents, null, LIBXML_NOCDATA );
 130  
 131          // Reset the error display to it's original setting.
 132          libxml_use_internal_errors( $errors );
 133  
 134          // Check to see if the XML parsing was successful.
 135          if ( ! is_object( $data ) ) {
 136              return false;
 137          }
 138  
 139          $entries = new Translations();
 140  
 141          // Loop through all of the single strings we found in the XML file.
 142          foreach ( $data->string as $string ) {
 143              // If the string is marked as non-translatable, skip it.
 144              if ( isset( $string['translatable'] ) && 'false' == $string['translatable'] ) {
 145                  continue;
 146              }
 147  
 148              // Generate the entry to add.
 149              $entry = $this->generate_entry( $string, (string) $string['name'] );
 150  
 151              // Add the entry to the results.
 152              $entries->add_entry( $entry );
 153          }
 154  
 155          // Loop through all of the multiple strings we found in the XML file.
 156          foreach ( $data->{'string-array'} as $string_array ) {
 157              if ( isset( $string_array['translatable'] ) && 'false' == $string_array['translatable'] ) {
 158                  continue;
 159              }
 160  
 161              $array_name = (string) $string_array['name'];
 162              $item_index = 0;
 163  
 164              foreach ( $string_array->item as $string ) {
 165                  // Generate the entry to add.
 166                  $entry = $this->generate_entry( $string, $array_name . "[$item_index]" );
 167  
 168                  // Add the entry to the results.
 169                  $entries->add_entry( $entry );
 170  
 171                  // Increment our index for the next entry.
 172                  $item_index++;
 173              }
 174          }
 175  
 176          return $entries;
 177      }
 178  
 179      /**
 180       * Generates a translation entry object to be added to the results for the "read_originals_from_file()" function.
 181       *
 182       * @since 1.0.0
 183       *
 184       * @param obj    $string  The string entry object to use.
 185       * @param string $context The context string to use.
 186       *
 187       * @return obj A translation entry object.
 188       */
 189  	private function generate_entry( $string, $context ) {
 190          // Check to see if there is an xliff tag in the string.
 191          $xliff_info = $this->extract_xliff_info( (string) $string[0] );
 192  
 193          // If an xliff tag was found, replace the translation and add a comment for later.
 194          if ( false !== $xliff_info ) {
 195              $string[0]          = $xliff_info['string'];
 196              $string['comment'] .= $xliff_info['description'];
 197          }
 198  
 199          // Create the new translation entry with the parsed data.
 200          $entry               = new Translation_Entry();
 201          $entry->context      = $context;
 202          $entry->singular     = $this->unescape( $string[0] );
 203          $entry->translations = array();
 204  
 205          // If we have a comment, add it to the entry.
 206          if ( isset( $string['comment'] ) && $string['comment'] ) {
 207              $entry->extracted_comments = (string) $string['comment'];
 208          }
 209  
 210          return $entry;
 211      }
 212  
 213      /**
 214       * Extracts the xliff information from a string.
 215       *
 216       * @since 1.0.0
 217       *
 218       * @param string $string The string to process.
 219       *
 220       * @return array|bool An array containing the extracted information from the xliff tags (there may be multiple) on success, false on failure.
 221       */
 222  	private function extract_xliff_info( $string ) {
 223          // Define the initial xliff tag to look for.
 224          $search = '--xlifftag--';
 225  
 226          /*
 227           * If it's not in the string, don't do any more processing.  Note we don't need to worry about
 228           * case sensitivity here as the search string was added before the XML processing was done.
 229           */
 230          if ( false === strstr( $string, $search ) ) {
 231              return false;
 232          }
 233  
 234          // Replace our temporary placeholder with the original text.
 235          $string = str_ireplace( $search, '<', $string );
 236  
 237          // Break apart the string in case there are multiple xliff's in it.
 238          $parts = explode( '<xliff:g', $string );
 239  
 240          // Setup the results array, part 0 will never need to be processed so automatically add it to the returned string.
 241          $result                = array();
 242          $result['string']      = $parts[0];
 243          $result['comment']     = '';
 244          $result['description'] = '';
 245  
 246          // As we can skip the first part, loop through only the remaining parts.
 247          $total = count( $parts );
 248          for ( $i = 1; $i < $total; $i++ ) {
 249              // Add back the part we stripped out during the explode() above.
 250              $current = '<xliff:g' . $parts[ $i ];
 251  
 252              $matches = array();
 253  
 254              /*
 255               * Break apart the entire string in to 5 parts:
 256               *
 257               *     0 = The full string.
 258               *     1 = Any text before the xliff tag.
 259               *     2 = The opening xliff tag.
 260               *     3 = The actual text to be translated.
 261               *     4 = The closing xliff tag.
 262               *     5 = The rest of the string.
 263               */
 264              if ( false !== preg_match( '/(.*)(<xliff:g.*>)(.*)(<\/xliff:g>)(.*)/i', $current, $matches ) ) {
 265                  // If we have a match add to the results parameters to return the correct parts of the match.
 266                  $result['string']  .= $matches[1] . $matches[3] . $matches[5];
 267                  $result['comment'] .= ' ' . $matches[2] . $matches[3] . $matches[4];
 268  
 269                  // Keep a copy of the current xliff tag that we're working with to parse for id/example attributes later.
 270                  $current_comment = $matches[2] . $matches[3] . $matches[4];
 271  
 272                  // Keep a copy of the component string to use later.
 273                  $component = $matches[3];
 274                  $text      = '';
 275  
 276                  // Parse the xliff tag for the id attribute, check for both single and double quotes.
 277                  $id = preg_match( '/.*id="(.*)".*/iU', $current_comment, $matches ) || preg_match( '/.*id=\'(.*)\'.*/iU', $current_comment, $matches );
 278  
 279                  // preg_match() returns int(1) when a match is found but since we're or'ing them, check to see if the result is a bool(true).
 280                  if ( true === $id ) {
 281                      // If an id attribute was found, record the contents of it.
 282                      $id = $matches[1];
 283                  } else {
 284                      // preg_match() can return either int(0) for not found or bool(false) on error, in either case let's make it a bool(false) for consistency later.
 285                      $id = false;
 286                  }
 287  
 288                  // Parse the xliff tag for the example attribute, check for both single and double quotes.
 289                  $example = preg_match( '/.*example="(.*)".*/iU', $current_comment, $matches ) || preg_match( '/.*example=\'(.*)\'.*/iU', $current_comment, $matches );
 290  
 291                  // preg_match() returns int(1) when a match is found but since we're or'ing them, check to see if the result is a bool(true).
 292                  if ( true === $example ) {
 293                      // If an example attribute was found, record the contents of it.
 294                      $example = $matches[1];
 295                  } else {
 296                      // preg_match() can return either int(0) for not found or bool(false) on error, in either case let's make it a bool(false) for consistency later.
 297                      $example = false;
 298                  }
 299  
 300                  // Time to make some human readable results based on what combination of id and example attributes that were found.
 301                  if ( false !== $id && false !== $example ) {
 302                      /* translators: 1: Component text 2: Component ID 3: Example output */
 303                      $text = sprintf( __( 'This string has content that should not be translated, the "%1$s" component of the original, which is identified as the "%2$s" attribute by the developer may be replaced at run time with text like this: %3$s', 'glotpress' ), $component, $id, $example );
 304                  } elseif ( false !== $id ) {
 305                      /* translators: 1: Component text 2: Example output */
 306                      $text = sprintf( __( 'This string has content that should not be translated, the "%1$s" component of the original, which is identified as the "%2$s" attribute by the developer and is not intended to be translated.', 'glotpress' ), $component, $id );
 307                  } elseif ( false !== $example ) {
 308                      /* translators: 1: Component ID 2: Example output */
 309                      $text = sprintf( __( 'This string has content that should not be translated, the "%1$s" component of the original may be replaced at run time with text like this: %2$s', 'glotpress' ), $component, $example );
 310                  } else {
 311                      /* translators: 1: Component ID */
 312                      $text = sprintf( __( 'This string has content that should not be translated, the "%1$s" component is not intended to be translated.', 'glotpress' ), $component );
 313                  }
 314  
 315                  // Add the description as set above to the return results array.
 316                  $result['description'] .= ' ' . $text;
 317              } else {
 318                  // If we don't, just append the current string to the result.
 319                  $result['string'] .= ' ' . $current;
 320              }
 321          }
 322  
 323          // Make sure to trim the comment and description before returning them.
 324          $result['comment']     = trim( $result['comment'] );
 325          $result['description'] = trim( $result['description'] );
 326  
 327          return $result;
 328      }
 329  
 330      /**
 331       * Save a line to the exported class variable.  Supports prepending of tabs and appending
 332       * a newline to the string.
 333       *
 334       * @since 1.0.0
 335       *
 336       * @param string $string       The string to process.
 337       * @param int    $prepend_tabs The number of tab characters to prepend to the output.
 338       */
 339  	private function line( $string, $prepend_tabs = 0 ) {
 340          $this->exported .= str_repeat( "\t", $prepend_tabs ) . "$string\n";
 341      }
 342  
 343      /**
 344       * Output the strings array entries to the exported class variable.
 345       *
 346       * @since 1.0.0
 347       *
 348       * @param obj $entries The entries to store.
 349       */
 350  	private function string_arrays( $entries ) {
 351          $mapping = array();
 352  
 353          // Sort the entries before processing them.
 354          uasort( $entries, array( $this, 'cmp_context' ) );
 355  
 356          // Loop through all of the single entries add them to a mapping array.
 357          foreach ( $entries as $entry ) {
 358              // Make sure the array name is sanitized.
 359              $array_name = preg_replace( '/\[\d+\]$/', '', $entry->context );
 360  
 361              // Initialize the mapping array entry if this is the first time.
 362              if ( ! isset( $mapping[ $array_name ] ) ) {
 363                  $mapping[ $array_name ] = array();
 364              }
 365  
 366              // Because Android doesn't fallback on the original locale
 367              // in string-arrays, we fill the non-translated ones with original locale string.
 368              $value = $entry->translations[0];
 369  
 370              // If we don't have a value for the translation, use the singular.
 371              if ( ! $value ) {
 372                  $value = $entry->singular;
 373              }
 374  
 375              // Add the entry to the mapping array after escaping it.
 376              $mapping[ $array_name ][] = $this->escape( $value );
 377          }
 378  
 379          // Now do the actual output to the class variable.
 380          foreach ( array_keys( $mapping ) as $array_name ) {
 381              // Open the string array tag.
 382              $this->line( '<string-array name="' . $array_name . '">', 1 );
 383  
 384              // Output each item in the array.
 385              foreach ( $mapping[ $array_name ] as $item ) {
 386                  $this->line( '<item>' . $item . '</item>', 2 );
 387              }
 388  
 389              // Close the string array tag.
 390              $this->line( '</string-array>', 1 );
 391          }
 392      }
 393  
 394      /**
 395       * Compare two context strings for a uasort callback.
 396       *
 397       * @since 1.0.0
 398       *
 399       * @param string $a The first string to compare.
 400       * @param string $b The second string to compare.
 401       *
 402       * @return int Returns the result of the comparison.
 403       */
 404  	private function cmp_context( $a, $b ) {
 405          return strnatcmp( $a->context, $b->context );
 406      }
 407  
 408      /**
 409       * Preserve a Unicode sequence (like \u1234) by adding another backslash.
 410       *
 411       * @since 3.0
 412       *
 413       * @param string $string The string to process.
 414       *
 415       * @return string Returns the string with double-escaped Unicode sequences.
 416       */
 417  	private function preserve_escaped_unicode( $string ) {
 418          return preg_replace( '#\\\\u([0-9a-fA-F]{4})#', '\\\\$0', $string );
 419      }
 420  
 421      /**
 422       * Unescapes a string with c style slashes.
 423       *
 424       * @since 1.0.0
 425       *
 426       * @param string $string The string to unescape.
 427       *
 428       * @return string Returns the unescaped string.
 429       */
 430  	private function unescape( $string ) {
 431          $string = $this->preserve_escaped_unicode( $string );
 432          return stripcslashes( $string );
 433      }
 434  
 435      /**
 436       * Escapes a string with c style slashes and html entities as required.
 437       *
 438       * @since 1.0.0
 439       *
 440       * @param string $string The string to escape.
 441       *
 442       * @return string Returns the escaped string.
 443       */
 444  	protected function escape( $string ) {
 445          $string = addcslashes( $string, "'\n\"" );
 446          $string = str_replace( array( '&', '<' ), array( '&amp;', '&lt;' ), $string );
 447  
 448          // Android strings that start with an '@' are references to other strings and need to be escaped.  See GH469.
 449          if ( gp_startswith( $string, '@' ) ) {
 450              $string = '\\' . $string;
 451          }
 452  
 453          return $string;
 454      }
 455  
 456  }
 457  
 458  GP::$formats['android'] = new GP_Format_Android();


Generated: Thu Apr 18 01:01:30 2024 Cross-referenced by PHPXref 0.7.1