$language) { if ($langcode != 'en' || l10n_update_english()) { $existing_languages[$langcode] = $language->name; } } // If we have no languages available, present the list of predefined languages // only. If we do have already added languages, set up two option groups with // the list of existing and then predefined languages. form_load_include($form_state, 'inc', 'language', 'language.admin'); if (empty($existing_languages)) { $language_options = language_admin_predefined_list(); $default = key($language_options); } else { $default = key($existing_languages); $language_options = array( t('Existing languages') => $existing_languages, t('Languages not yet added') => language_admin_predefined_list() ); } $validators = array( 'file_validate_extensions' => array('po'), 'file_validate_size' => array(file_upload_max_size()), ); $form['file'] = array( '#type' => 'file', '#title' => t('Translation file'), '#description' => theme('file_upload_help', array('description' => t('A Gettext Portable Object file.'), 'upload_validators' => $validators)), '#size' => 50, '#upload_validators' => $validators, '#attributes' => array('class' => array('file-import-input')), '#attached' => array( 'js' => array( drupal_get_path('module', 'locale') . '/locale.bulk.js' => array(), ), ), ); $form['langcode'] = array( '#type' => 'select', '#title' => t('Language'), '#options' => $language_options, '#default_value' => $default, '#attributes' => array('class' => array('langcode-input')), ); $form['customized'] = array( '#title' => t('Treat imported strings as custom translations'), '#type' => 'checkbox', ); $form['overwrite_options'] = array( '#type' => 'container', '#tree' => TRUE, ); $form['overwrite_options']['not_customized'] = array( '#title' => t('Overwrite non-customized translations'), '#type' => 'checkbox', '#states' => array( 'checked' => array( ':input[name="customized"]' => array('checked' => TRUE), ), ), ); $form['overwrite_options']['customized'] = array( '#title' => t('Overwrite existing customized translations'), '#type' => 'checkbox', ); $form['actions'] = array( '#type' => 'actions' ); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Import') ); return $form; } /** * Form submission handler for l10n_update_import_form(). */ function l10n_update_import_form_submit($form, &$form_state) { // Ensure we have the file uploaded. if ($file = file_save_upload('file', $form_state, $form['file']['#upload_validators'], 'translations://', 0)) { // Add language, if not yet supported. $language = language_load($form_state['values']['langcode']); if (empty($language)) { $language = new Language(array( 'id' => $form_state['values']['langcode'] )); $language = language_save($language); drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name)))); } $options = array( 'langcode' => $form_state['values']['langcode'], 'overwrite_options' => $form_state['values']['overwrite_options'], 'customized' => $form_state['values']['customized'] ? L10N_UPDATE_CUSTOMIZED : L10N_UPDATE_NOT_CUSTOMIZED, ); $file = l10n_update_file_attach_properties($file, $options); $batch = l10n_update_batch_build(array($file->uri => $file), $options); batch_set($batch); } else { form_set_error('file', $form_state, t('File to import not found.')); $form_state['rebuild'] = TRUE; return; } $form_state['redirect_route']['route_name'] = 'locale.translate_page'; return; } /** * Form constructor for the Gettext translation files export form. * * @see l10n_update_export_form_submit() * @ingroup forms */ function l10n_update_export_form($form, &$form_state) { global $language; $languages = language_list(); $language_options = array(); foreach ($languages as $langcode => $language) { if ($langcode != 'en' || l10n_update_english()) { $language_options[$langcode] = $language->name; } } $language_default = language_default(); if (empty($language_options)) { $form['langcode'] = array( '#type' => 'value', '#value' => $language->language, ); $form['langcode_text'] = array( '#type' => 'item', '#title' => t('Language'), '#markup' => t('No language available. The export will only contain source strings.'), ); } else { $form['langcode'] = array( '#type' => 'select', '#title' => t('Language'), '#options' => $language_options, '#default_value' => $language_default->id, '#empty_option' => t('Source text only, no translations'), '#empty_value' => $language->language, ); $form['content_options'] = array( '#type' => 'details', '#title' => t('Export options'), '#collapsed' => TRUE, '#tree' => TRUE, '#states' => array( 'invisible' => array( ':input[name="langcode"]' => array('value' => $language->language), ), ), ); $form['content_options']['not_customized'] = array( '#type' => 'checkbox', '#title' => t('Include non-customized translations'), '#default_value' => TRUE, ); $form['content_options']['customized'] = array( '#type' => 'checkbox', '#title' => t('Include customized translations'), '#default_value' => TRUE, ); $form['content_options']['not_translated'] = array( '#type' => 'checkbox', '#title' => t('Include untranslated text'), '#default_value' => TRUE, ); } $form['actions'] = array( '#type' => 'actions' ); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Export') ); return $form; } /** * Form submission handler for l10n_update_export_form(). */ function l10n_update_export_form_submit($form, &$form_state) { global $language; // If template is required, language code is not given. if ($form_state['values']['langcode'] != $language->language) { $languages = language_list(); $language = isset($languages[$form_state['values']['langcode']]) ? $languages[$form_state['values']['langcode']] : NULL; } else { $language = NULL; } $content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array(); $reader = new PoDatabaseReader(); $languageName = ''; if ($language != NULL) { $reader->setLangcode($language->id); $reader->setOptions($content_options); $languages = language_list(); $languageName = isset($languages[$language->id]) ? $languages[$language->id]->name : ''; $filename = $language->id .'.po'; } else { // Template required. $filename = 'drupal.pot'; } $item = $reader->readItem(); if (!empty($item)) { $uri = tempnam('temporary://', 'po_'); $header = $reader->getHeader(); $header->setProjectName(variable_get('site_name')); $header->setLanguageName($languageName); $writer = new PoStreamWriter; $writer->setUri($uri); $writer->setHeader($header); $writer->open(); $writer->writeItem($item); $writer->writeItems($reader); $writer->close(); } else { drupal_set_message('Nothing to export.'); } } /** * Prepare a batch to import all translations. * * @param array $options * An array with options that can have the following elements: * - 'langcode': The language code. Optional, defaults to NULL, which means * that the language will be detected from the name of the files. * - 'overwrite_options': Overwrite options array as defined in * PoDatabaseWriter. Optional, defaults to an empty array. * - 'customized': Flag indicating whether the strings imported from $file * are customized translations or come from a community source. Use * L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to * L10N_UPDATE_NOT_CUSTOMIZED. * - 'finish_feedback': Whether or not to give feedback to the user when the * batch is finished. Optional, defaults to TRUE. * * @param $force * (optional) Import all available files, even if they were imported before. * * @todo * Integrate with update status to identify projects needed and integrate * l10n_update functionality to feed in translation files alike. * See http://drupal.org/node/1191488. */ function l10n_update_batch_import_files($options, $force = FALSE) { $options += array( 'overwrite_options' => array(), 'customized' => L10N_UPDATE_NOT_CUSTOMIZED, 'finish_feedback' => TRUE, ); if (!empty($options['langcode'])) { $langcodes = array($options['langcode']); } else { // If langcode was not provided, make sure to only import files for the // languages we have enabled. $langcodes = array_keys(language_list()); } $files = l10n_update_get_interface_translation_files(array(), $langcodes); if (!$force) { $result = db_select('l10n_update_file', 'lf') ->fields('lf', array('langcode', 'uri', 'timestamp')) ->condition('language', $langcodes) ->execute() ->fetchAllAssoc('uri'); foreach ($result as $uri => $info) { if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) { // The file is already imported and not changed since the last import. // Remove it from file list and don't import it again. unset($files[$uri]); } } } return l10n_update_batch_build($files, $options); } /** * Get interface translation files present in the translations directory. * * @param array $projects * Project names from which to get the translation files and history. * Defaults to all projects. * @param array $langcodes * Language codes from which to get the translation files and history. * Defaults to all languagues * * @return array * An array of interface translation files keyed by their URI. */ function l10n_update_get_interface_translation_files($projects = array(), $langcodes = array()) { module_load_include('compare.inc', 'l10n_update'); $files = array(); $projects = $projects ? $projects : array_keys(l10n_update_get_projects()); $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list()); // Scan the translations directory for files matching a name pattern // containing a project name and language code: {project}.{langcode}.po or // {project}-{version}.{langcode}.po. // Only files of known projects and languages will be returned. $directory = variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH); $result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', array('recurse' => FALSE)); foreach ($result as $file) { // Update the file object with project name and version from the file name. $file = l10n_update_file_attach_properties($file); if (in_array($file->project, $projects)) { if (in_array($file->langcode, $langcodes)) { $files[$file->uri] = $file; } } } return $files; } /** * Build a locale batch from an array of files. * * @param $files * Array of file objects to import. * * @param array $options * An array with options that can have the following elements: * - 'langcode': The language code. Optional, defaults to NULL, which means * that the language will be detected from the name of the files. * - 'overwrite_options': Overwrite options array as defined in * PoDatabaseWriter. Optional, defaults to an empty array. * - 'customized': Flag indicating whether the strings imported from $file * are customized translations or come from a community source. Use * L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to * L10N_UPDATE_NOT_CUSTOMIZED. * - 'finish_feedback': Whether or not to give feedback to the user when the * batch is finished. Optional, defaults to TRUE. * * @return * A batch structure or FALSE if $files was empty. */ function l10n_update_batch_build($files, $options) { $options += array( 'overwrite_options' => array(), 'customized' => L10N_UPDATE_NOT_CUSTOMIZED, 'finish_feedback' => TRUE, ); if (count($files)) { $operations = array(); foreach ($files as $file) { // We call l10n_update_batch_import for every batch operation. $operations[] = array('l10n_update_batch_import', array($file, $options)); } // Save the translation status of all files. $operations[] = array('l10n_update_batch_import_save', array()); // Add a final step to refresh JavaScript and configuration strings. $operations[] = array('l10n_update_batch_refresh', array()); $batch = array( 'operations' => $operations, 'title' => t('Importing interface translations'), 'progress_message' => '', 'error_message' => t('Error importing interface translations'), 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc', ); if ($options['finish_feedback']) { $batch['finished'] = 'l10n_update_batch_finished'; } return $batch; } return FALSE; } /** * Perform interface translation import as a batch step. * * @param object $file * A file object of the gettext file to be imported. The file object must * contain a language parameter. This is used as the language of the import. * * @param array $options * An array with options that can have the following elements: * - 'langcode': The language code. * - 'overwrite_options': Overwrite options array as defined in * PoDatabaseWriter. Optional, defaults to an empty array. * - 'customized': Flag indicating whether the strings imported from $file * are customized translations or come from a community source. Use * L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to * L10N_UPDATE_NOT_CUSTOMIZED. * - 'message': Alternative message to display during import. Note, this must * be sanitized text. * * @param $context * Contains a list of files imported. */ function l10n_update_batch_import($file, $options, &$context) { // Merge the default values in the $options array. $options += array( 'overwrite_options' => array(), 'customized' => L10N_UPDATE_NOT_CUSTOMIZED, ); if (isset($file->langcode)) { try { if (empty($context['sandbox'])) { $context['sandbox']['parse_state'] = array( 'filesize' => filesize(drupal_realpath($file->uri)), 'chunk_size' => 200, 'seek' => 0, ); } // Update the seek and the number of items in the $options array(). $options['seek'] = $context['sandbox']['parse_state']['seek']; $options['items'] = $context['sandbox']['parse_state']['chunk_size']; $report = Gettext::fileToDatabase($file, $options); // If not yet finished with reading, mark progress based on size and // position. if ($report['seek'] < filesize($file->uri)) { $context['sandbox']['parse_state']['seek'] = $report['seek']; // Maximize the progress bar at 95% before completion, the batch API // could trigger the end of the operation before file reading is done, // because of floating point inaccuracies. See // http://drupal.org/node/1089472 $context['finished'] = min(0.95, $report['seek'] / filesize($file->uri)); if (isset($options['message'])) { $context['message'] = t('!message (@percent%).', array('!message' => $options['message'], '@percent' => (int) ($context['finished'] * 100))); } else { $context['message'] = t('Importing translation file: %filename (@percent%).', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100))); } } else { // We are finished here. $context['finished'] = 1; // Store the file data for processing by the next batch operation. $file->timestamp = filemtime($file->uri); $context['results']['files'][$file->uri] = $file; $context['results']['languages'][$file->uri] = $file->langcode; } // Add the reported values to the statistics for this file. // Each import iteration reports statistics in an array. The results of // each iteration are added and merged here and stored per file. if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) { $context['results']['stats'][$file->uri] = array(); } foreach ($report as $key => $value) { if (is_numeric($report[$key])) { if (!isset($context['results']['stats'][$file->uri][$key])) { $context['results']['stats'][$file->uri][$key] = 0; } $context['results']['stats'][$file->uri][$key] += $report[$key]; } elseif (is_array($value)) { $context['results']['stats'][$file->uri] += array($key => array()); $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value); } } } catch (Exception $exception) { // Import failed. Store the data of the failing file. $context['results']['failed_files'][] = $file; watchdog('l10n_update', 'Unable to import translations file: @file (@message)', array('@file' => $file->uri, '@message' => $exception->getMessage())); } } } /** * Batch callback: Save data of imported files. * * @param $context * Contains a list of imported files. */ function l10n_update_batch_import_save($context) { if (isset($context['results']['files'])) { foreach ($context['results']['files'] as $file) { // Update the file history if both project and version are known. This // table is used by the automated translation update function which tracks // translation status of module and themes in the system. Other // translation files are not tracked and are therefore not stored in this // table. if ($file->project && $file->version) { $file->last_checked = REQUEST_TIME; l10n_update_update_file_history($file); } } $context['message'] = t('Translations imported.'); } } /** * Refreshs translations after importing strings. * * @param array $context * Contains a list of strings updated and information about the progress. */ function l10n_update_batch_refresh(array &$context) { if (!isset($context['sandbox']['refresh'])) { $strings = $langcodes = array(); if (isset($context['results']['stats'])) { // Get list of unique string identifiers and language codes updated. $langcodes = array_unique(array_values($context['results']['languages'])); foreach ($context['results']['stats'] as $report) { $strings = array_merge($strings, $report['strings']); } } if ($strings) { // Initialize multi-step string refresh. $context['message'] = t('Updating translations for JavaScript and configuration strings.'); $context['sandbox']['refresh']['strings'] = array_unique($strings); $context['sandbox']['refresh']['languages'] = $langcodes; $context['sandbox']['refresh']['names'] = array(); $context['results']['stats']['config'] = 0; $context['sandbox']['refresh']['count'] = count($strings); // We will update strings on later steps. $context['finished'] = 1 - 1 / $context['sandbox']['refresh']['count']; } else { $context['finished'] = 1; } } elseif (!empty($context['sandbox']['refresh']['strings'])) { // Not perfect but will give some indication of progress. $context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count']; // Pending strings, refresh 100 at a time, get next pack. $next = array_slice($context['sandbox']['refresh']['strings'], 0, 100); array_splice($context['sandbox']['refresh']['strings'], 0, count($next)); // Clear cache and force refresh of JavaScript translations. _l10n_update_refresh_translations($context['sandbox']['refresh']['languages'], $next); } else { $context['finished'] = 1; } } /** * Finished callback of system page locale import batch. */ function l10n_update_batch_finished($success, $results) { if ($success) { $additions = $updates = $deletes = $skips = $config = 0; if (isset($results['failed_files'])) { if (module_exists('dblog')) { $message = format_plural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.', array('@url' => url('admin/reports/dblog'))); } else { $message = format_plural(count($results['failed_files']), 'One translation files could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.'); } drupal_set_message($message, 'error'); } if (isset($results['files'])) { $skipped_files = array(); // If there are no results and/or no stats (eg. coping with an empty .po // file), simply do nothing. if ($results && isset($results['stats'])) { foreach ($results['stats'] as $filepath => $report) { $additions += $report['additions']; $updates += $report['updates']; $deletes += $report['deletes']; $skips += $report['skips']; if ($report['skips'] > 0) { $skipped_files[] = $filepath; } } } drupal_set_message(format_plural(count($results['files']), 'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', '@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes) )); watchdog('l10n_update', 'Translations imported: %number added, %update updated, %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)); if ($skips) { if (module_exists('dblog')) { $message = format_plural($skips, 'One 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.', array('@url' => url('admin/reports/dblog'))); } else { $message = format_plural($skips, 'One 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($message, 'warning'); watchdog('l10n_update', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING); } } } } /** * Creates a file object and populates the timestamp property. * * @param $filepath * The filepath of a file to import. * * @return * An object representing the file. */ function l10n_update_file_create($filepath) { $file = new stdClass(); $file->filename = drupal_basename($filepath); $file->uri = $filepath; $file->timestamp = filemtime($file->uri); return $file; } /** * Generates file properties from filename and options. * * An attempt is made to determine the translation language, project name and * project version from the file name. Supported file name patterns are: * {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po. * Alternatively the translation language can be set using the $options. * * @param object $file * A file object of the gettext file to be imported. * @param array $options * An array with options: * - 'langcode': The language code. Overrides the file language. * * @return object * Modified file object. */ function l10n_update_file_attach_properties($file, $options = array()) { // If $file is a file entity, convert it to a stdClass. if ($file instanceof FileInterface) { $file = (object) array( 'filename' => $file->getFilename(), 'uri' => $file->getFileUri(), ); } // Extract project, version and language code from the file name. Supported: // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po preg_match('! ( # project OR project and version OR emtpy (group 1) ([a-z_]+) # project name (group 2) \. # . | # OR ([a-z_]+) # project name (group 3) \- # - ([0-9a-z\.\-\+]+) # version (group 4) \. # . | # OR ) # (empty) ([^\./]+) # language code (group 5) \. # . po # po extension $!x', $file->filename, $matches); if (isset($matches[5])) { $file->project = $matches[2] . $matches[3]; $file->version = $matches[4]; $file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5]; } return $file; } /** * Deletes interface translation files and translation history records. * * @param array $projects * Project names from which to delete the translation files and history. * Defaults to all projects. * @param array $langcodes * Language codes from which to delete the translation files and history. * Defaults to all languagues * * @return boolean * TRUE if files are removed successfully. FALSE if one or more files could * not be deleted. */ function l10n_update_delete_translation_files($projects = array(), $langcodes = array()) { $fail = FALSE; l10n_update_file_history_delete($projects, $langcodes); // Delete all translation files from the translations directory. if ($files = l10n_update_get_interface_translation_files($projects, $langcodes)) { foreach ($files as $file) { $success = file_unmanaged_delete($file->uri); if (!$success) { $fail = TRUE; } } } return !$fail; }