[ Index ] |
PHP Cross Reference of GlotPress |
[Summary view] [Print] [Text view]
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( '&', '<' ), $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();
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Nov 21 01:01:07 2024 | Cross-referenced by PHPXref 0.7.1 |