exported = '';
$this->line( '' );
$this->line( '' );
$this->line( '' );
$string_array_items = array();
foreach ( $entries as $entry ) {
if ( preg_match( '/.+\[\d+\]$/', $entry->context ) ) {
// Array item found.
$string_array_items[] = $entry;
continue;
}
if ( empty( $entry->context ) ) {
$entry->context = $entry->singular;
}
$id = preg_replace( '/[^a-zA-Z0-9_]/U', '_', $entry->context );
$this->line( '' . $this->escape( $entry->translations[0] ) . '', 1 );
}
$this->string_arrays( $string_array_items );
$this->line( '' );
return $this->exported;
}
/**
* Reads a set of original strings from an Android XML file.
*
* @since 1.0.0
*
* @param string $file_name The name of the uploaded Android XML file.
*
* @return Translations|bool The extracted originals on success, false on failure.
*/
public function read_originals_from_file( $file_name ) {
// Disable the output of errors while processing the XML file.
$errors = libxml_use_internal_errors( true );
// Get the contents from the temporary file.
$contents = file_get_contents( $file_name );
/*
* Android strings can use tags to indicate a part of the string should NOT be translated.
*
* See the "Mark message parts that should not be translated" section of https://developer.android.com/distribute/tools/localization-checklist.html
*
* Unfortunately SimpleXML will parse these as valid XML tags, which we don't want so replace the opening brace with something we can
* re-instate later to process the xliff tags ourselves.
*/
$contents = str_ireplace( '', '--xlifftag--/xliff:g>', $contents );
// Parse the file contents.
$data = simplexml_load_string( $contents, null, LIBXML_NOCDATA );
// Reset the error display to it's original setting.
libxml_use_internal_errors( $errors );
// Check to see if the XML parsing was successful.
if ( ! is_object( $data ) ) {
return false;
}
$entries = new Translations();
// Loop through all of the single strings we found in the XML file.
foreach ( $data->string as $string ) {
// If the string is marked as non-translatable, skip it.
if ( isset( $string['translatable'] ) && 'false' == $string['translatable'] ) {
continue;
}
// Generate the entry to add.
$entry = $this->generate_entry( $string, (string) $string['name'] );
// Add the entry to the results.
$entries->add_entry( $entry );
}
// Loop through all of the multiple strings we found in the XML file.
foreach ( $data->{'string-array'} as $string_array ) {
if ( isset( $string_array['translatable'] ) && 'false' == $string_array['translatable'] ) {
continue;
}
$array_name = (string) $string_array['name'];
$item_index = 0;
foreach ( $string_array->item as $string ) {
// Generate the entry to add.
$entry = $this->generate_entry( $string, $array_name . "[$item_index]" );
// Add the entry to the results.
$entries->add_entry( $entry );
// Increment our index for the next entry.
$item_index++;
}
}
return $entries;
}
/**
* Generates a translation entry object to be added to the results for the "read_originals_from_file()" function.
*
* @since 1.0.0
*
* @param obj $string The string entry object to use.
* @param string $context The context string to use.
*
* @return obj A translation entry object.
*/
private function generate_entry( $string, $context ) {
// Check to see if there is an xliff tag in the string.
$xliff_info = $this->extract_xliff_info( (string) $string[0] );
// If an xliff tag was found, replace the translation and add a comment for later.
if ( false !== $xliff_info ) {
$string[0] = $xliff_info['string'];
$string['comment'] .= $xliff_info['description'];
}
// Create the new translation entry with the parsed data.
$entry = new Translation_Entry();
$entry->context = $context;
$entry->singular = $this->unescape( $string[0] );
$entry->translations = array();
// If we have a comment, add it to the entry.
if ( isset( $string['comment'] ) && $string['comment'] ) {
$entry->extracted_comments = (string) $string['comment'];
}
return $entry;
}
/**
* Extracts the xliff information from a string.
*
* @since 1.0.0
*
* @param string $string The string to process.
*
* @return array|bool An array containing the extracted information from the xliff tags (there may be multiple) on success, false on failure.
*/
private function extract_xliff_info( $string ) {
// Define the initial xliff tag to look for.
$search = '--xlifftag--';
/*
* If it's not in the string, don't do any more processing. Note we don't need to worry about
* case sensitivity here as the search string was added before the XML processing was done.
*/
if ( false === strstr( $string, $search ) ) {
return false;
}
// Replace our temporary placeholder with the original text.
$string = str_ireplace( $search, '<', $string );
// Break apart the string in case there are multiple xliff's in it.
$parts = explode( ')(.*)(<\/xliff:g>)(.*)/i', $current, $matches ) ) {
// If we have a match add to the results parameters to return the correct parts of the match.
$result['string'] .= $matches[1] . $matches[3] . $matches[5];
$result['comment'] .= ' ' . $matches[2] . $matches[3] . $matches[4];
// Keep a copy of the current xliff tag that we're working with to parse for id/example attributes later.
$current_comment = $matches[2] . $matches[3] . $matches[4];
// Keep a copy of the component string to use later.
$component = $matches[3];
$text = '';
// Parse the xliff tag for the id attribute, check for both single and double quotes.
$id = preg_match( '/.*id="(.*)".*/iU', $current_comment, $matches ) || preg_match( '/.*id=\'(.*)\'.*/iU', $current_comment, $matches );
// 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).
if ( true === $id ) {
// If an id attribute was found, record the contents of it.
$id = $matches[1];
} else {
// 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.
$id = false;
}
// Parse the xliff tag for the example attribute, check for both single and double quotes.
$example = preg_match( '/.*example="(.*)".*/iU', $current_comment, $matches ) || preg_match( '/.*example=\'(.*)\'.*/iU', $current_comment, $matches );
// 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).
if ( true === $example ) {
// If an example attribute was found, record the contents of it.
$example = $matches[1];
} else {
// 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.
$example = false;
}
// Time to make some human readable results based on what combination of id and example attributes that were found.
if ( false !== $id && false !== $example ) {
/* translators: 1: Component text 2: Component ID 3: Example output */
$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 );
} elseif ( false !== $id ) {
/* translators: 1: Component text 2: Example output */
$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 );
} elseif ( false !== $example ) {
/* translators: 1: Component ID 2: Example output */
$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 );
} else {
/* translators: 1: Component ID */
$text = sprintf( __( 'This string has content that should not be translated, the "%1$s" component is not intended to be translated.', 'glotpress' ), $component );
}
// Add the description as set above to the return results array.
$result['description'] .= ' ' . $text;
} else {
// If we don't, just append the current string to the result.
$result['string'] .= ' ' . $current;
}
}
// Make sure to trim the comment and description before returning them.
$result['comment'] = trim( $result['comment'] );
$result['description'] = trim( $result['description'] );
return $result;
}
/**
* Save a line to the exported class variable. Supports prepending of tabs and appending
* a newline to the string.
*
* @since 1.0.0
*
* @param string $string The string to process.
* @param int $prepend_tabs The number of tab characters to prepend to the output.
*/
private function line( $string, $prepend_tabs = 0 ) {
$this->exported .= str_repeat( "\t", $prepend_tabs ) . "$string\n";
}
/**
* Output the strings array entries to the exported class variable.
*
* @since 1.0.0
*
* @param obj $entries The entries to store.
*/
private function string_arrays( $entries ) {
$mapping = array();
// Sort the entries before processing them.
uasort( $entries, array( $this, 'cmp_context' ) );
// Loop through all of the single entries add them to a mapping array.
foreach ( $entries as $entry ) {
// Make sure the array name is sanitized.
$array_name = preg_replace( '/\[\d+\]$/', '', $entry->context );
// Initialize the mapping array entry if this is the first time.
if ( ! isset( $mapping[ $array_name ] ) ) {
$mapping[ $array_name ] = array();
}
// Because Android doesn't fallback on the original locale
// in string-arrays, we fill the non-translated ones with original locale string.
$value = $entry->translations[0];
// If we don't have a value for the translation, use the singular.
if ( ! $value ) {
$value = $entry->singular;
}
// Add the entry to the mapping array after escaping it.
$mapping[ $array_name ][] = $this->escape( $value );
}
// Now do the actual output to the class variable.
foreach ( array_keys( $mapping ) as $array_name ) {
// Open the string array tag.
$this->line( '', 1 );
// Output each item in the array.
foreach ( $mapping[ $array_name ] as $item ) {
$this->line( '- ' . $item . '
', 2 );
}
// Close the string array tag.
$this->line( '', 1 );
}
}
/**
* Compare two context strings for a uasort callback.
*
* @since 1.0.0
*
* @param string $a The first string to compare.
* @param string $b The second string to compare.
*
* @return int Returns the result of the comparison.
*/
private function cmp_context( $a, $b ) {
return strnatcmp( $a->context, $b->context );
}
/**
* Preserve a Unicode sequence (like \u1234) by adding another backslash.
*
* @since 3.0
*
* @param string $string The string to process.
*
* @return string Returns the string with double-escaped Unicode sequences.
*/
private function preserve_escaped_unicode( $string ) {
return preg_replace( '#\\\\u([0-9a-fA-F]{4})#', '\\\\$0', $string );
}
/**
* Unescapes a string with c style slashes.
*
* @since 1.0.0
*
* @param string $string The string to unescape.
*
* @return string Returns the unescaped string.
*/
private function unescape( $string ) {
$string = $this->preserve_escaped_unicode( $string );
return stripcslashes( $string );
}
/**
* Escapes a string with c style slashes and html entities as required.
*
* @since 1.0.0
*
* @param string $string The string to escape.
*
* @return string Returns the escaped string.
*/
protected function escape( $string ) {
$string = addcslashes( $string, "'\n\"" );
$string = str_replace( array( '&', '<' ), array( '&', '<' ), $string );
// Android strings that start with an '@' are references to other strings and need to be escaped. See GH469.
if ( gp_startswith( $string, '@' ) ) {
$string = '\\' . $string;
}
return $string;
}
}
GP::$formats['android'] = new GP_Format_Android();