2011-08-27 22:04:48 +00:00
< ? php
/**
* @ file
* Gettext parsing and generating API .
*
* @ todo Decouple these functions from Locale API and put to gettext_ namespace .
*/
/**
* @ defgroup locale - api - import - export Translation import / export API .
* @ {
* Functions to import and export translations .
*
* These functions provide the ability to import translations from
* external files and to export translations and translation templates .
*/
/**
2012-02-19 03:41:24 +00:00
* Parses Gettext Portable Object information and inserts it into the database .
2011-08-27 22:04:48 +00:00
*
* @ param $file
* Drupal file object corresponding to the PO file to import .
* @ param $langcode
* Language code .
2012-04-09 18:24:12 +00:00
* @ param $overwrite_options
* An associative array indicating what data should be overwritten , if any .
* - not_customized : strings marked not customized should be overwritten .
* - customized : strings marked customized should be overwritten .
* @ param $customized
* Whether the strings being imported should be saved as customized .
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED . All strings in the file
* will be saved with this customization flag .
2011-08-27 22:04:48 +00:00
*/
2012-04-09 18:24:12 +00:00
function _locale_import_po ( $file , $langcode , $overwrite_options , $customized = LOCALE_NOT_CUSTOMIZED ) {
2011-08-27 22:04:48 +00:00
// Try to allocate enough time to parse and import the data.
drupal_set_time_limit ( 240 );
// Check if we have the language already in the database.
2011-10-21 06:42:30 +00:00
if ( ! language_load ( $langcode )) {
2011-08-27 22:04:48 +00:00
drupal_set_message ( t ( 'The language selected for import is not supported.' ), 'error' );
return FALSE ;
}
// Get strings from file (returns on failure after a partial import, or on success)
2012-04-09 18:24:12 +00:00
$status = _locale_import_read_po ( 'db-store' , $file , $overwrite_options , $langcode , $customized );
2011-08-27 22:04:48 +00:00
if ( $status === FALSE ) {
// Error messages are set in _locale_import_read_po().
return FALSE ;
}
// Get status information on import process.
list ( $header_done , $additions , $updates , $deletes , $skips ) = _locale_import_one_string ( 'db-report' );
if ( ! $header_done ) {
drupal_set_message ( t ( 'The translation file %filename appears to have a missing or malformed header.' , array ( '%filename' => $file -> filename )), 'error' );
}
// Clear cache and force refresh of JavaScript translations.
_locale_invalidate_js ( $langcode );
2011-09-11 16:14:18 +00:00
cache () -> deletePrefix ( 'locale:' );
2011-08-27 22:04:48 +00:00
// Rebuild the menu, strings may have changed.
2012-05-03 15:09:39 +00:00
menu_router_rebuild ();
2011-08-27 22:04:48 +00:00
drupal_set_message ( t ( 'The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.' , array ( '%number' => $additions , '%update' => $updates , '%delete' => $deletes )));
watchdog ( 'locale' , 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.' , array ( '%file' => $file -> filename , '%locale' => $langcode , '%number' => $additions , '%update' => $updates , '%delete' => $deletes ));
if ( $skips ) {
2011-10-29 09:14:20 +00:00
if ( module_exists ( 'dblog' )) {
$skip_message = format_plural ( $skips , 'A translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.' , '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.' , array ( '@url' => url ( 'admin/reports/dblog' )));
}
else {
$skip_message = format_plural ( $skips , 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.' , '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.' );
}
drupal_set_message ( $skip_message , 'error' );
2011-08-27 22:04:48 +00:00
watchdog ( 'locale' , '@count disallowed HTML string(s) in %file' , array ( '@count' => $skips , '%file' => $file -> uri ), WATCHDOG_WARNING );
}
return TRUE ;
}
/**
2012-02-19 03:41:24 +00:00
* Parses a Gettext Portable Object file into an array .
2011-08-27 22:04:48 +00:00
*
* @ param $op
* Storage operation type : db - store or mem - store .
* @ param $file
* Drupal file object corresponding to the PO file to import .
2012-04-09 18:24:12 +00:00
* @ param $overwrite_options
* An associative array indicating what data should be overwritten , if any .
* - not_customized : not customized strings should be overwritten .
* - customized : customized strings should be overwritten .
2011-08-27 22:04:48 +00:00
* @ param $lang
* Language code .
2012-04-09 18:24:12 +00:00
* @ param $customized
* Whether the strings being imported should be saved as customized .
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED .
2011-08-27 22:04:48 +00:00
*/
2012-04-09 18:24:12 +00:00
function _locale_import_read_po ( $op , $file , $overwrite_options = NULL , $lang = NULL , $customized = LOCALE_NOT_CUSTOMIZED ) {
2011-08-27 22:04:48 +00:00
// The file will get closed by PHP on returning from this function.
$fd = fopen ( $file -> uri , 'rb' );
if ( ! $fd ) {
2011-10-01 19:47:01 +00:00
_locale_import_message ( 'The translation import failed because the file %filename could not be read.' , $file );
2011-08-27 22:04:48 +00:00
return FALSE ;
}
/*
* The parser context . Can be :
* - 'COMMENT' ( #)
* - 'MSGID' ( msgid )
* - 'MSGID_PLURAL' ( msgid_plural )
* - 'MSGCTXT' ( msgctxt )
* - 'MSGSTR' ( msgstr or msgstr [])
* - 'MSGSTR_ARR' ( msgstr_arg )
*/
$context = 'COMMENT' ;
// Current entry being read.
$current = array ();
// Current plurality for 'msgstr[]'.
$plural = 0 ;
// Current line.
$lineno = 0 ;
while ( ! feof ( $fd )) {
// A line should not be longer than 10 * 1024.
$line = fgets ( $fd , 10 * 1024 );
if ( $lineno == 0 ) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace ( " \xEF \xBB \xBF " , '' , $line );
}
$lineno ++ ;
// Trim away the linefeed.
$line = trim ( strtr ( $line , array ( " \\ \n " => " " )));
if ( ! strncmp ( '#' , $line , 1 )) {
// Lines starting with '#' are comments.
if ( $context == 'COMMENT' ) {
// Already in comment token, insert the comment.
$current [ '#' ][] = substr ( $line , 1 );
}
elseif (( $context == 'MSGSTR' ) || ( $context == 'MSGSTR_ARR' )) {
// We are currently in string token, close it out.
2012-04-09 18:24:12 +00:00
_locale_import_one_string ( $op , $current , $overwrite_options , $lang , $file , $customized );
2011-08-27 22:04:48 +00:00
// Start a new entry for the comment.
$current = array ();
$current [ '#' ][] = substr ( $line , 1 );
$context = 'COMMENT' ;
}
else {
// A comment following any other token is a syntax error.
_locale_import_message ( 'The translation file %filename contains an error: "msgstr" was expected but not found on line %line.' , $file , $lineno );
return FALSE ;
}
}
elseif ( ! strncmp ( 'msgid_plural' , $line , 12 )) {
// A plural form for the current message.
if ( $context != 'MSGID' ) {
// A plural form cannot be added to anything else but the id directly.
_locale_import_message ( 'The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.' , $file , $lineno );
return FALSE ;
}
// Remove 'msgid_plural' and trim away whitespace.
$line = trim ( substr ( $line , 12 ));
// At this point, $line should now contain only the plural form.
$quoted = _locale_import_parse_quoted ( $line );
if ( $quoted === FALSE ) {
// The plural form must be wrapped in quotes.
_locale_import_message ( 'The translation file %filename contains a syntax error on line %line.' , $file , $lineno );
return FALSE ;
}
// Append the plural form to the current entry.
2012-03-11 02:35:21 +00:00
$current [ 'msgid' ] .= LOCALE_PLURAL_DELIMITER . $quoted ;
2011-08-27 22:04:48 +00:00
$context = 'MSGID_PLURAL' ;
}
elseif ( ! strncmp ( 'msgid' , $line , 5 )) {
// Starting a new message.
if (( $context == 'MSGSTR' ) || ( $context == 'MSGSTR_ARR' )) {
// We are currently in a message string, close it out.
2012-04-09 18:24:12 +00:00
_locale_import_one_string ( $op , $current , $overwrite_options , $lang , $file , $customized );
2011-08-27 22:04:48 +00:00
// Start a new context for the id.
$current = array ();
}
elseif ( $context == 'MSGID' ) {
// We are currently already in the context, meaning we passed an id with no data.
_locale_import_message ( 'The translation file %filename contains an error: "msgid" is unexpected on line %line.' , $file , $lineno );
return FALSE ;
}
// Remove 'msgid' and trim away whitespace.
$line = trim ( substr ( $line , 5 ));
// At this point, $line should now contain only the message id.
$quoted = _locale_import_parse_quoted ( $line );
if ( $quoted === FALSE ) {
// The message id must be wrapped in quotes.
_locale_import_message ( 'The translation file %filename contains a syntax error on line %line.' , $file , $lineno );
return FALSE ;
}
$current [ 'msgid' ] = $quoted ;
$context = 'MSGID' ;
}
elseif ( ! strncmp ( 'msgctxt' , $line , 7 )) {
// Starting a new context.
if (( $context == 'MSGSTR' ) || ( $context == 'MSGSTR_ARR' )) {
// We are currently in a message, start a new one.
2012-04-09 18:24:12 +00:00
_locale_import_one_string ( $op , $current , $overwrite_options , $lang , $file , $customized );
2011-08-27 22:04:48 +00:00
$current = array ();
}
elseif ( ! empty ( $current [ 'msgctxt' ])) {
// A context cannot apply to another context.
_locale_import_message ( 'The translation file %filename contains an error: "msgctxt" is unexpected on line %line.' , $file , $lineno );
return FALSE ;
}
// Remove 'msgctxt' and trim away whitespaces.
$line = trim ( substr ( $line , 7 ));
// At this point, $line should now contain the context.
$quoted = _locale_import_parse_quoted ( $line );
if ( $quoted === FALSE ) {
// The context string must be quoted.
_locale_import_message ( 'The translation file %filename contains a syntax error on line %line.' , $file , $lineno );
return FALSE ;
}
$current [ 'msgctxt' ] = $quoted ;
$context = 'MSGCTXT' ;
}
elseif ( ! strncmp ( 'msgstr[' , $line , 7 )) {
// A message string for a specific plurality.
if (( $context != 'MSGID' ) && ( $context != 'MSGCTXT' ) && ( $context != 'MSGID_PLURAL' ) && ( $context != 'MSGSTR_ARR' )) {
// Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries.
_locale_import_message ( 'The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.' , $file , $lineno );
return FALSE ;
}
// Ensure the plurality is terminated.
if ( strpos ( $line , ']' ) === FALSE ) {
_locale_import_message ( 'The translation file %filename contains a syntax error on line %line.' , $file , $lineno );
return FALSE ;
}
// Extract the plurality.
$frombracket = strstr ( $line , '[' );
$plural = substr ( $frombracket , 1 , strpos ( $frombracket , ']' ) - 1 );
// Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data.
$line = trim ( strstr ( $line , " " ));
$quoted = _locale_import_parse_quoted ( $line );
if ( $quoted === FALSE ) {
// The string must be quoted.
_locale_import_message ( 'The translation file %filename contains a syntax error on line %line.' , $file , $lineno );
return FALSE ;
}
$current [ 'msgstr' ][ $plural ] = $quoted ;
$context = 'MSGSTR_ARR' ;
}
elseif ( ! strncmp ( " msgstr " , $line , 6 )) {
// A string for the an id or context.
if (( $context != 'MSGID' ) && ( $context != 'MSGCTXT' )) {
// Strings are only valid within an id or context scope.
_locale_import_message ( 'The translation file %filename contains an error: "msgstr" is unexpected on line %line.' , $file , $lineno );
return FALSE ;
}
// Remove 'msgstr' and trim away away whitespaces.
$line = trim ( substr ( $line , 6 ));
// At this point, $line should now contain the message.
$quoted = _locale_import_parse_quoted ( $line );
if ( $quoted === FALSE ) {
// The string must be quoted.
_locale_import_message ( 'The translation file %filename contains a syntax error on line %line.' , $file , $lineno );
return FALSE ;
}
$current [ 'msgstr' ] = $quoted ;
$context = 'MSGSTR' ;
}
elseif ( $line != '' ) {
// Anything that is not a token may be a continuation of a previous token.
$quoted = _locale_import_parse_quoted ( $line );
if ( $quoted === FALSE ) {
// The string must be quoted.
_locale_import_message ( 'The translation file %filename contains a syntax error on line %line.' , $file , $lineno );
return FALSE ;
}
// Append the string to the current context.
if (( $context == 'MSGID' ) || ( $context == 'MSGID_PLURAL' )) {
$current [ 'msgid' ] .= $quoted ;
}
elseif ( $context == 'MSGCTXT' ) {
$current [ 'msgctxt' ] .= $quoted ;
}
elseif ( $context == 'MSGSTR' ) {
$current [ 'msgstr' ] .= $quoted ;
}
elseif ( $context == 'MSGSTR_ARR' ) {
$current [ 'msgstr' ][ $plural ] .= $quoted ;
}
else {
// No valid context to append to.
_locale_import_message ( 'The translation file %filename contains an error: there is an unexpected string on line %line.' , $file , $lineno );
return FALSE ;
}
}
}
// End of PO file, closed out the last entry.
if (( $context == 'MSGSTR' ) || ( $context == 'MSGSTR_ARR' )) {
2012-04-09 18:24:12 +00:00
_locale_import_one_string ( $op , $current , $overwrite_options , $lang , $file , $customized );
2011-08-27 22:04:48 +00:00
}
elseif ( $context != 'COMMENT' ) {
_locale_import_message ( 'The translation file %filename ended unexpectedly at line %line.' , $file , $lineno );
return FALSE ;
}
}
/**
2012-02-19 03:41:24 +00:00
* Sets an error message if an error occurred during locale file parsing .
2011-08-27 22:04:48 +00:00
*
* @ param $message
* The message to be translated .
* @ param $file
* Drupal file object corresponding to the PO file to import .
* @ param $lineno
* An optional line number argument .
*/
function _locale_import_message ( $message , $file , $lineno = NULL ) {
$vars = array ( '%filename' => $file -> filename );
if ( isset ( $lineno )) {
$vars [ '%line' ] = $lineno ;
}
$t = get_t ();
drupal_set_message ( $t ( $message , $vars ), 'error' );
}
/**
2012-02-19 03:41:24 +00:00
* Performs the specified operation for one string .
2011-08-27 22:04:48 +00:00
*
* @ param $op
* Operation to perform : 'db-store' , 'db-report' , 'mem-store' or 'mem-report' .
* @ param $value
* Details of the string stored .
2012-04-09 18:24:12 +00:00
* @ param $overwrite_options
* An associative array indicating what data should be overwritten , if any .
* - not_customized : not customized strings should be overwritten .
* - customized : customized strings should be overwritten .
2011-08-27 22:04:48 +00:00
* @ param $lang
* Language to store the string in .
* @ param $file
* Object representation of file being imported , only required when op is
* 'db-store' .
2012-04-09 18:24:12 +00:00
* @ param $customized
* Whether the strings being imported should be saved as customized .
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED .
2011-08-27 22:04:48 +00:00
*/
2012-04-09 18:24:12 +00:00
function _locale_import_one_string ( $op , $value = NULL , $overwrite_options = NULL , $lang = NULL , $file = NULL , $customized = LOCALE_NOT_CUSTOMIZED ) {
2011-08-27 22:04:48 +00:00
$report = & drupal_static ( __FUNCTION__ , array ( 'additions' => 0 , 'updates' => 0 , 'deletes' => 0 , 'skips' => 0 ));
$header_done = & drupal_static ( __FUNCTION__ . ':header_done' , FALSE );
$strings = & drupal_static ( __FUNCTION__ . ':strings' , array ());
switch ( $op ) {
// Return stored strings
case 'mem-report' :
return $strings ;
// Store string in memory (only supports single strings)
case 'mem-store' :
$strings [ isset ( $value [ 'msgctxt' ]) ? $value [ 'msgctxt' ] : '' ][ $value [ 'msgid' ]] = $value [ 'msgstr' ];
return ;
// Called at end of import to inform the user
case 'db-report' :
return array ( $header_done , $report [ 'additions' ], $report [ 'updates' ], $report [ 'deletes' ], $report [ 'skips' ]);
// Store the string we got in the database.
case 'db-store' :
2012-03-11 02:35:21 +00:00
2011-08-27 22:04:48 +00:00
if ( $value [ 'msgid' ] == '' ) {
2012-03-11 02:35:21 +00:00
// If 'msgid' is empty, it means we got values for the header of the
// file as per the structure of the Gettext format.
2011-11-09 04:25:48 +00:00
$locale_plurals = variable_get ( 'locale_translation_plurals' , array ());
2012-04-09 18:24:12 +00:00
if ( array_sum ( $overwrite_options ) || empty ( $locale_plurals [ $lang ][ 'plurals' ])) {
2011-08-27 22:04:48 +00:00
// Since we only need to parse the header if we ought to update the
// plural formula, only run this if we don't need to keep existing
// data untouched or if we don't have an existing plural formula.
$header = _locale_import_parse_header ( $value [ 'msgstr' ]);
2012-02-21 09:38:03 +00:00
// Get and store the plural formula if available.
2011-08-27 22:04:48 +00:00
if ( isset ( $header [ " Plural-Forms " ]) && $p = _locale_import_parse_plural_forms ( $header [ " Plural-Forms " ], $file -> uri )) {
2011-11-09 04:25:48 +00:00
list ( $nplurals , $formula ) = $p ;
2012-02-21 09:38:03 +00:00
$locale_plurals [ $lang ] = array (
'plurals' => $nplurals ,
'formula' => $formula ,
);
variable_set ( 'locale_translation_plurals' , $locale_plurals );
2011-08-27 22:04:48 +00:00
}
}
$header_done = TRUE ;
}
else {
2012-03-11 02:35:21 +00:00
// Found a string to store, clean up and prepare the data.
2011-08-27 22:04:48 +00:00
$comments = _locale_import_shorten_comments ( empty ( $value [ '#' ]) ? array () : $value [ '#' ]);
2012-03-11 02:35:21 +00:00
if ( is_array ( $value [ 'msgstr' ])) {
// Sort plural variants by their form index.
ksort ( $value [ 'msgstr' ]);
// Serialize plural variants in one string by LOCALE_PLURAL_DELIMITER.
$value [ 'msgstr' ] = implode ( LOCALE_PLURAL_DELIMITER , $value [ 'msgstr' ]);
2011-08-27 22:04:48 +00:00
}
2012-03-11 02:35:21 +00:00
_locale_import_one_string_db (
$report ,
$lang ,
isset ( $value [ 'msgctxt' ]) ? $value [ 'msgctxt' ] : '' ,
$value [ 'msgid' ],
$value [ 'msgstr' ],
$comments ,
2012-04-09 18:24:12 +00:00
$overwrite_options ,
$customized
2012-03-11 02:35:21 +00:00
);
2011-08-27 22:04:48 +00:00
}
} // end of db-store operation
}
/**
2012-02-19 03:41:24 +00:00
* Imports one string into the database .
2011-08-27 22:04:48 +00:00
*
* @ param $report
* Report array summarizing the number of changes done in the form :
* array ( inserts , updates , deletes ) .
* @ param $langcode
* Language code to import string into .
* @ param $context
* The context of this string .
* @ param $source
* Source string .
* @ param $translation
* Translation to language specified in $langcode .
* @ param $location
* Location value to save with source string .
2012-04-09 18:24:12 +00:00
* @ param $overwrite_options
* An associative array indicating what data should be overwritten , if any .
* - not_customized : not customized strings should be overwritten .
* - customized : customized strings should be overwritten .
* @ param $customized
* ( optional ) Whether the strings being imported should be saved as customized .
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED .
2011-08-27 22:04:48 +00:00
*
* @ return
* The string ID of the existing string modified or the new string added .
*/
2012-04-09 18:24:12 +00:00
function _locale_import_one_string_db ( & $report , $langcode , $context , $source , $translation , $location , $overwrite_options , $customized = LOCALE_NOT_CUSTOMIZED ) {
// Initialize overwrite options if not set.
$overwrite_options += array (
'not_customized' => FALSE ,
'customized' => FALSE ,
);
// Look up the source string and any existing translation.
$string = db_query ( " SELECT s.lid, t.customized FROM { locales_source} s LEFT JOIN { locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context " , array (
':source' => $source ,
':context' => $context ,
':language' => $langcode ,
))
-> fetchObject ();
2011-08-27 22:04:48 +00:00
if ( ! empty ( $translation )) {
// Skip this string unless it passes a check for dangerous code.
if ( ! locale_string_is_safe ( $translation )) {
2011-10-29 09:14:20 +00:00
watchdog ( 'locale' , 'Import of string "%string" was skipped because of disallowed or malformed HTML.' , array ( '%string' => $translation ), WATCHDOG_ERROR );
2011-08-27 22:04:48 +00:00
$report [ 'skips' ] ++ ;
2012-04-09 18:24:12 +00:00
return 0 ;
2011-08-27 22:04:48 +00:00
}
2012-04-09 18:24:12 +00:00
elseif ( isset ( $string -> lid )) {
2011-08-27 22:04:48 +00:00
// We have this source string saved already.
db_update ( 'locales_source' )
-> fields ( array (
'location' => $location ,
))
2012-04-09 18:24:12 +00:00
-> condition ( 'lid' , $string -> lid )
2011-08-27 22:04:48 +00:00
-> execute ();
2012-04-09 18:24:12 +00:00
if ( ! isset ( $string -> customized )) {
2011-08-27 22:04:48 +00:00
// No translation in this language.
db_insert ( 'locales_target' )
-> fields ( array (
2012-04-09 18:24:12 +00:00
'lid' => $string -> lid ,
2011-08-27 22:04:48 +00:00
'language' => $langcode ,
'translation' => $translation ,
2012-04-09 18:24:12 +00:00
'customized' => $customized ,
2011-08-27 22:04:48 +00:00
))
-> execute ();
$report [ 'additions' ] ++ ;
}
2012-04-09 18:24:12 +00:00
elseif ( $overwrite_options [ $string -> customized ? 'customized' : 'not_customized' ]) {
2011-08-27 22:04:48 +00:00
// Translation exists, only overwrite if instructed.
db_update ( 'locales_target' )
-> fields ( array (
'translation' => $translation ,
2012-04-09 18:24:12 +00:00
'customized' => $customized ,
2011-08-27 22:04:48 +00:00
))
-> condition ( 'language' , $langcode )
2012-04-09 18:24:12 +00:00
-> condition ( 'lid' , $string -> lid )
2011-08-27 22:04:48 +00:00
-> execute ();
$report [ 'updates' ] ++ ;
}
2012-04-09 18:24:12 +00:00
return $string -> lid ;
2011-08-27 22:04:48 +00:00
}
else {
// No such source string in the database yet.
$lid = db_insert ( 'locales_source' )
-> fields ( array (
'location' => $location ,
'source' => $source ,
'context' => ( string ) $context ,
))
-> execute ();
db_insert ( 'locales_target' )
-> fields ( array (
'lid' => $lid ,
'language' => $langcode ,
'translation' => $translation ,
2012-04-09 18:24:12 +00:00
'customized' => $customized ,
2011-08-27 22:04:48 +00:00
))
-> execute ();
$report [ 'additions' ] ++ ;
2012-04-09 18:24:12 +00:00
return $lid ;
2011-08-27 22:04:48 +00:00
}
}
2012-04-09 18:24:12 +00:00
elseif ( isset ( $string -> lid ) && isset ( $string -> customized ) && $overwrite_options [ $string -> customized ? 'customized' : 'not_customized' ]) {
2011-08-27 22:04:48 +00:00
// Empty translation, remove existing if instructed.
db_delete ( 'locales_target' )
-> condition ( 'language' , $langcode )
2012-04-09 18:24:12 +00:00
-> condition ( 'lid' , $string -> lid )
2011-08-27 22:04:48 +00:00
-> execute ();
$report [ 'deletes' ] ++ ;
2012-04-09 18:24:12 +00:00
return $string -> lid ;
2011-08-27 22:04:48 +00:00
}
}
/**
2012-02-19 03:41:24 +00:00
* Parses a Gettext Portable Object file header .
2011-08-27 22:04:48 +00:00
*
* @ param $header
* A string containing the complete header .
*
* @ return
* An associative array of key - value pairs .
*/
function _locale_import_parse_header ( $header ) {
$header_parsed = array ();
$lines = array_map ( 'trim' , explode ( " \n " , $header ));
foreach ( $lines as $line ) {
if ( $line ) {
list ( $tag , $contents ) = explode ( " : " , $line , 2 );
$header_parsed [ trim ( $tag )] = trim ( $contents );
}
}
return $header_parsed ;
}
/**
2012-02-19 03:41:24 +00:00
* Parses a Plural - Forms entry from a Gettext Portable Object file header .
2011-08-27 22:04:48 +00:00
*
* @ param $pluralforms
* A string containing the Plural - Forms entry .
* @ param $filepath
* A string containing the filepath .
*
* @ return
* An array containing the number of plurals and a
* formula in PHP for computing the plural form .
*/
function _locale_import_parse_plural_forms ( $pluralforms , $filepath ) {
// First, delete all whitespace
$pluralforms = strtr ( $pluralforms , array ( " " => " " , " \t " => " " ));
// Select the parts that define nplurals and plural
$nplurals = strstr ( $pluralforms , " nplurals= " );
if ( strpos ( $nplurals , " ; " )) {
$nplurals = substr ( $nplurals , 9 , strpos ( $nplurals , " ; " ) - 9 );
}
else {
return FALSE ;
}
$plural = strstr ( $pluralforms , " plural= " );
if ( strpos ( $plural , " ; " )) {
$plural = substr ( $plural , 7 , strpos ( $plural , " ; " ) - 7 );
}
else {
return FALSE ;
}
// Get PHP version of the plural formula
$plural = _locale_import_parse_arithmetic ( $plural );
if ( $plural !== FALSE ) {
return array ( $nplurals , $plural );
}
else {
drupal_set_message ( t ( 'The translation file %filepath contains an error: the plural formula could not be parsed.' , array ( '%filepath' => $filepath )), 'error' );
return FALSE ;
}
}
/**
2012-02-19 03:41:24 +00:00
* Parses and sanitizes an arithmetic formula into a PHP expression .
2011-08-27 22:04:48 +00:00
*
* While parsing , we ensure , that the operators have the right
* precedence and associativity .
*
* @ param $string
* A string containing the arithmetic formula .
*
* @ return
* The PHP version of the formula .
*/
function _locale_import_parse_arithmetic ( $string ) {
// Operator precedence table
$precedence = array ( " ( " => - 1 , " ) " => - 1 , " ? " => 1 , " : " => 1 , " || " => 3 , " && " => 4 , " == " => 5 , " != " => 5 , " < " => 6 , " > " => 6 , " <= " => 6 , " >= " => 6 , " + " => 7 , " - " => 7 , " * " => 8 , " / " => 8 , " % " => 8 );
// Right associativity
$right_associativity = array ( " ? " => 1 , " : " => 1 );
$tokens = _locale_import_tokenize_formula ( $string );
// Parse by converting into infix notation then back into postfix
// Operator stack - holds math operators and symbols
$operator_stack = array ();
// Element Stack - holds data to be operated on
$element_stack = array ();
foreach ( $tokens as $token ) {
$current_token = $token ;
// Numbers and the $n variable are simply pushed into $element_stack
if ( is_numeric ( $token )) {
$element_stack [] = $current_token ;
}
elseif ( $current_token == " n " ) {
$element_stack [] = '$n' ;
}
elseif ( $current_token == " ( " ) {
$operator_stack [] = $current_token ;
}
elseif ( $current_token == " ) " ) {
$topop = array_pop ( $operator_stack );
while ( isset ( $topop ) && ( $topop != " ( " )) {
$element_stack [] = $topop ;
$topop = array_pop ( $operator_stack );
}
}
elseif ( ! empty ( $precedence [ $current_token ])) {
// If it's an operator, then pop from $operator_stack into $element_stack until the
// precedence in $operator_stack is less than current, then push into $operator_stack
$topop = array_pop ( $operator_stack );
while ( isset ( $topop ) && ( $precedence [ $topop ] >= $precedence [ $current_token ]) && ! (( $precedence [ $topop ] == $precedence [ $current_token ]) && ! empty ( $right_associativity [ $topop ]) && ! empty ( $right_associativity [ $current_token ]))) {
$element_stack [] = $topop ;
$topop = array_pop ( $operator_stack );
}
if ( $topop ) {
$operator_stack [] = $topop ; // Return element to top
}
$operator_stack [] = $current_token ; // Parentheses are not needed
}
else {
return FALSE ;
}
}
// Flush operator stack
$topop = array_pop ( $operator_stack );
while ( $topop != NULL ) {
$element_stack [] = $topop ;
$topop = array_pop ( $operator_stack );
}
// Now extract formula from stack
$previous_size = count ( $element_stack ) + 1 ;
while ( count ( $element_stack ) < $previous_size ) {
$previous_size = count ( $element_stack );
for ( $i = 2 ; $i < count ( $element_stack ); $i ++ ) {
$op = $element_stack [ $i ];
if ( ! empty ( $precedence [ $op ])) {
$f = " " ;
if ( $op == " : " ) {
$f = $element_stack [ $i - 2 ] . " ): " . $element_stack [ $i - 1 ] . " ) " ;
}
elseif ( $op == " ? " ) {
$f = " ( " . $element_stack [ $i - 2 ] . " ?( " . $element_stack [ $i - 1 ];
}
else {
$f = " ( " . $element_stack [ $i - 2 ] . $op . $element_stack [ $i - 1 ] . " ) " ;
}
array_splice ( $element_stack , $i - 2 , 3 , $f );
break ;
}
}
}
// If only one element is left, the number of operators is appropriate
if ( count ( $element_stack ) == 1 ) {
return $element_stack [ 0 ];
}
else {
return FALSE ;
}
}
/**
2012-02-19 03:41:24 +00:00
* Provides backward - compatible formula parsing for token_get_all () .
2011-08-27 22:04:48 +00:00
*
* @ param $string
* A string containing the arithmetic formula .
*
* @ return
* The PHP version of the formula .
*/
function _locale_import_tokenize_formula ( $formula ) {
$formula = str_replace ( " " , " " , $formula );
$tokens = array ();
for ( $i = 0 ; $i < strlen ( $formula ); $i ++ ) {
if ( is_numeric ( $formula [ $i ])) {
$num = $formula [ $i ];
$j = $i + 1 ;
while ( $j < strlen ( $formula ) && is_numeric ( $formula [ $j ])) {
$num .= $formula [ $j ];
$j ++ ;
}
$i = $j - 1 ;
$tokens [] = $num ;
}
elseif ( $pos = strpos ( " =<>!&| " , $formula [ $i ])) { // We won't have a space
$next = $formula [ $i + 1 ];
switch ( $pos ) {
case 1 :
case 2 :
case 3 :
case 4 :
if ( $next == '=' ) {
$tokens [] = $formula [ $i ] . '=' ;
$i ++ ;
}
else {
$tokens [] = $formula [ $i ];
}
break ;
case 5 :
if ( $next == '&' ) {
$tokens [] = '&&' ;
$i ++ ;
}
else {
$tokens [] = $formula [ $i ];
}
break ;
case 6 :
if ( $next == '|' ) {
$tokens [] = '||' ;
$i ++ ;
}
else {
$tokens [] = $formula [ $i ];
}
break ;
}
}
else {
$tokens [] = $formula [ $i ];
}
}
return $tokens ;
}
/**
2012-02-19 03:41:24 +00:00
* Generates a short , one - string version of the passed comment array .
2011-08-27 22:04:48 +00:00
*
* @ param $comment
* An array of strings containing a comment .
*
* @ return
2012-02-19 03:41:24 +00:00
* Short one - string version of the comment .
2011-08-27 22:04:48 +00:00
*/
function _locale_import_shorten_comments ( $comment ) {
$comm = '' ;
while ( count ( $comment )) {
$test = $comm . substr ( array_shift ( $comment ), 1 ) . ', ' ;
if ( strlen ( $comm ) < 130 ) {
$comm = $test ;
}
else {
break ;
}
}
return trim ( substr ( $comm , 0 , - 2 ));
}
/**
2012-02-19 03:41:24 +00:00
* Parses a string in quotes .
2011-08-27 22:04:48 +00:00
*
* @ param $string
* A string specified with enclosing quotes .
*
* @ return
* The string parsed from inside the quotes .
*/
function _locale_import_parse_quoted ( $string ) {
if ( substr ( $string , 0 , 1 ) != substr ( $string , - 1 , 1 )) {
return FALSE ; // Start and end quotes must be the same
}
$quote = substr ( $string , 0 , 1 );
$string = substr ( $string , 1 , - 1 );
if ( $quote == '"' ) { // Double quotes: strip slashes
return stripcslashes ( $string );
}
elseif ( $quote == " ' " ) { // Simple quote: return as-is
return $string ;
}
else {
return FALSE ; // Unrecognized quote
}
}
/**
2012-02-19 03:41:24 +00:00
* Generates a structured array of all translated strings for the language .
2011-08-27 22:04:48 +00:00
*
* @ param $language
* Language object to generate the output for , or NULL if generating
* translation template .
2012-04-09 18:24:12 +00:00
* @ param $options
* ( optional ) An associative array specifying what to include in the output :
* - customized : include customized strings ( if TRUE )
* - uncustomized : include non - customized string ( if TRUE )
* - untranslated : include untranslated source strings ( if TRUE )
* Ignored if $language is NULL .
2012-02-19 03:41:24 +00:00
*
* @ return
* An array of translated strings that can be used to generate an export .
2011-08-27 22:04:48 +00:00
*/
2012-04-09 18:24:12 +00:00
function _locale_export_get_strings ( $language = NULL , $options = array ()) {
// Assume FALSE for all options if not provided by the API.
$options += array (
'customized' => FALSE ,
'not_customized' => FALSE ,
'not_translated' => FALSE ,
);
if ( array_sum ( $options ) == 0 ) {
// If user asked to not include anything in the translation files,
// that would not make sense, so just fall back on providing a template.
$language = NULL ;
}
// Build and execute query to collect source strings and translations.
$query = db_select ( 'locales_source' , 's' );
if ( ! empty ( $language )) {
if ( $options [ 'not_translated' ]) {
// Left join to keep untranslated strings in.
$query -> leftJoin ( 'locales_target' , 't' , 's.lid = t.lid AND t.language = :language' , array ( ':language' => $language -> langcode ));
}
else {
// Inner join to filter for only translations.
$query -> innerJoin ( 'locales_target' , 't' , 's.lid = t.lid AND t.language = :language' , array ( ':language' => $language -> langcode ));
}
if ( $options [ 'customized' ]) {
if ( ! $options [ 'not_customized' ]) {
// Filter for customized strings only.
$query -> condition ( 't.customized' , LOCALE_CUSTOMIZED );
}
// Else no filtering needed in this case.
}
else {
if ( $options [ 'not_customized' ]) {
// Filter for non-customized strings only.
$query -> condition ( 't.customized' , LOCALE_NOT_CUSTOMIZED );
}
else {
// Filter for strings without translation.
$query -> isNull ( 't.translation' );
}
}
$query -> fields ( 't' , array ( 'translation' ));
2011-08-27 22:04:48 +00:00
}
else {
2012-04-09 18:24:12 +00:00
$query -> leftJoin ( 'locales_target' , 't' , 's.lid = t.lid' );
2011-08-27 22:04:48 +00:00
}
2012-04-09 18:24:12 +00:00
$query -> fields ( 's' , array ( 'lid' , 'source' , 'context' , 'location' ));
$result = $query -> execute ();
// Structure results in an array with metainformation on the strings.
2011-08-27 22:04:48 +00:00
$strings = array ();
foreach ( $result as $child ) {
2012-03-11 02:35:21 +00:00
$strings [ $child -> lid ] = array (
2011-08-27 22:04:48 +00:00
'comment' => $child -> location ,
'source' => $child -> source ,
'context' => $child -> context ,
'translation' => isset ( $child -> translation ) ? $child -> translation : '' ,
);
}
return $strings ;
}
/**
2012-02-19 03:41:24 +00:00
* Generates the PO ( T ) file contents for the given strings .
2011-08-27 22:04:48 +00:00
*
* @ param $language
* Language object to generate the output for , or NULL if generating
* translation template .
* @ param $strings
* Array of strings to export . See _locale_export_get_strings ()
* on how it should be formatted .
* @ param $header
* The header portion to use for the output file . Defaults
* are provided for PO and POT files .
*/
function _locale_export_po_generate ( $language = NULL , $strings = array (), $header = NULL ) {
global $user ;
2011-11-09 04:25:48 +00:00
$locale_plurals = variable_get ( 'locale_translation_plurals' , array ());
2011-08-27 22:04:48 +00:00
if ( ! isset ( $header )) {
if ( isset ( $language )) {
2012-07-02 17:20:33 +00:00
$header = '# ' . $language -> name . ' translation of ' . config ( 'system.site' ) -> get ( 'name' ) . " \n " ;
2011-08-27 22:04:48 +00:00
$header .= '# Generated by ' . $user -> name . ' <' . $user -> mail . " > \n " ;
$header .= " # \n " ;
$header .= " msgid \" \" \n " ;
$header .= " msgstr \" \" \n " ;
$header .= " \" Project-Id-Version: PROJECT VERSION \\ n \" \n " ;
$header .= " \" POT-Creation-Date: " . date ( " Y-m-d H:iO " ) . " \\ n \" \n " ;
$header .= " \" PO-Revision-Date: " . date ( " Y-m-d H:iO " ) . " \\ n \" \n " ;
$header .= " \" Last-Translator: NAME <EMAIL@ADDRESS> \\ n \" \n " ;
$header .= " \" Language-Team: LANGUAGE <EMAIL@ADDRESS> \\ n \" \n " ;
$header .= " \" MIME-Version: 1.0 \\ n \" \n " ;
$header .= " \" Content-Type: text/plain; charset=utf-8 \\ n \" \n " ;
$header .= " \" Content-Transfer-Encoding: 8bit \\ n \" \n " ;
2012-01-10 15:29:08 +00:00
if ( ! empty ( $locale_plurals [ $language -> langcode ][ 'formula' ])) {
$header .= " \" Plural-Forms: nplurals= " . $locale_plurals [ $language -> langcode ][ 'plurals' ] . " ; plural= " . strtr ( $locale_plurals [ $language -> langcode ][ 'formula' ], array ( '$' => '' )) . " ; \\ n \" \n " ;
2012-03-11 02:35:21 +00:00
// Remember number of plural variants to optimize the export.
$nplurals = $locale_plurals [ $language -> langcode ][ 'plurals' ];
}
else {
// Remember we did not have a plural number for the export.
$nplurals = 0 ;
2011-08-27 22:04:48 +00:00
}
}
else {
$header = " # LANGUAGE translation of PROJECT \n " ;
$header .= " # Copyright (c) YEAR NAME <EMAIL@ADDRESS> \n " ;
$header .= " # \n " ;
$header .= " msgid \" \" \n " ;
$header .= " msgstr \" \" \n " ;
$header .= " \" Project-Id-Version: PROJECT VERSION \\ n \" \n " ;
$header .= " \" POT-Creation-Date: " . date ( " Y-m-d H:iO " ) . " \\ n \" \n " ;
$header .= " \" PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ \\ n \" \n " ;
$header .= " \" Last-Translator: NAME <EMAIL@ADDRESS> \\ n \" \n " ;
$header .= " \" Language-Team: LANGUAGE <EMAIL@ADDRESS> \\ n \" \n " ;
$header .= " \" MIME-Version: 1.0 \\ n \" \n " ;
$header .= " \" Content-Type: text/plain; charset=utf-8 \\ n \" \n " ;
$header .= " \" Content-Transfer-Encoding: 8bit \\ n \" \n " ;
$header .= " \" Plural-Forms: nplurals=INTEGER; plural=EXPRESSION; \\ n \" \n " ;
}
}
$output = $header . " \n " ;
foreach ( $strings as $lid => $string ) {
2012-03-11 02:35:21 +00:00
if ( $string [ 'comment' ]) {
$output .= '#: ' . $string [ 'comment' ] . " \n " ;
}
if ( ! empty ( $string [ 'context' ])) {
$output .= 'msgctxt ' . _locale_export_string ( $string [ 'context' ]);
}
if ( strpos ( $string [ 'source' ], LOCALE_PLURAL_DELIMITER ) !== FALSE ) {
// Export plural string.
$export_array = explode ( LOCALE_PLURAL_DELIMITER , $string [ 'source' ]);
$output .= 'msgid ' . _locale_export_string ( $export_array [ 0 ]);
$output .= 'msgid_plural ' . _locale_export_string ( $export_array [ 1 ]);
if ( isset ( $language )) {
$export_array = explode ( LOCALE_PLURAL_DELIMITER , $string [ 'translation' ]);
for ( $i = 0 ; $i < $nplurals ; $i ++ ) {
if ( isset ( $export_array [ $i ])) {
$output .= 'msgstr[' . $i . '] ' . _locale_export_string ( $export_array [ $i ]);
}
else {
$output .= 'msgstr[' . $i . '] ""' . " \n " ;
2011-08-27 22:04:48 +00:00
}
}
}
else {
2012-03-11 02:35:21 +00:00
$output .= 'msgstr[0] ""' . " \n " ;
$output .= 'msgstr[1] ""' . " \n " ;
2011-08-27 22:04:48 +00:00
}
}
2012-03-11 02:35:21 +00:00
else {
$output .= 'msgid ' . _locale_export_string ( $string [ 'source' ]);
$output .= 'msgstr ' . _locale_export_string ( $string [ 'translation' ]);
}
$output .= " \n " ;
2011-08-27 22:04:48 +00:00
}
return $output ;
}
/**
2012-02-19 03:41:24 +00:00
* Writes a generated PO or POT file to the output .
2011-08-27 22:04:48 +00:00
*
* @ param $language
* Language object to generate the output for , or NULL if generating
* translation template .
* @ param $output
* The PO ( T ) file to output as a string . See _locale_export_generate_po ()
* on how it can be generated .
*/
function _locale_export_po ( $language = NULL , $output = NULL ) {
// Log the export event.
if ( isset ( $language )) {
2012-01-10 15:29:08 +00:00
$filename = $language -> langcode . '.po' ;
2011-08-27 22:04:48 +00:00
watchdog ( 'locale' , 'Exported %locale translation file: %filename.' , array ( '%locale' => $language -> name , '%filename' => $filename ));
}
else {
$filename = 'drupal.pot' ;
watchdog ( 'locale' , 'Exported translation file: %filename.' , array ( '%filename' => $filename ));
}
// Download the file for the client.
header ( " Content-Disposition: attachment; filename= $filename " );
header ( " Content-Type: text/plain; charset=utf-8 " );
print $output ;
drupal_exit ();
}
/**
2012-02-19 03:41:24 +00:00
* Prints a string on multiple lines .
2011-08-27 22:04:48 +00:00
*/
function _locale_export_string ( $str ) {
$stri = addcslashes ( $str , " \0 .. \37 \\ \" " );
$parts = array ();
// Cut text into several lines
while ( $stri != " " ) {
$i = strpos ( $stri , " \\ n " );
if ( $i === FALSE ) {
$curstr = $stri ;
$stri = " " ;
}
else {
$curstr = substr ( $stri , 0 , $i + 2 );
$stri = substr ( $stri , $i + 2 );
}
$curparts = explode ( " \n " , _locale_export_wrap ( $curstr , 70 ));
$parts = array_merge ( $parts , $curparts );
}
// Multiline string
if ( count ( $parts ) > 1 ) {
return " \" \" \n \" " . implode ( " \" \n \" " , $parts ) . " \" \n " ;
}
// Single line string
elseif ( count ( $parts ) == 1 ) {
return " \" $parts[0] \" \n " ;
}
// No translation
else {
return " \" \" \n " ;
}
}
/**
2012-02-19 03:41:24 +00:00
* Wraps text for Portable Object ( Template ) files .
2011-08-27 22:04:48 +00:00
*/
function _locale_export_wrap ( $str , $len ) {
$words = explode ( ' ' , $str );
$return = array ();
$cur = " " ;
$nstr = 1 ;
while ( count ( $words )) {
$word = array_shift ( $words );
if ( $nstr ) {
$cur = $word ;
$nstr = 0 ;
}
elseif ( strlen ( " $cur $word " ) > $len ) {
$return [] = $cur . " " ;
$cur = $word ;
}
else {
$cur = " $cur $word " ;
}
}
$return [] = $cur ;
return implode ( " \n " , $return );
}
/**
2012-05-17 12:58:49 +00:00
* @ } End of " defgroup locale-api-import-export " .
2011-08-27 22:04:48 +00:00
*/