l10n_update.bulk.inc in Localization update 7.2
Mass import-export and batch import functionality for Gettext .po files.
File
l10n_update.bulk.incView source
<?php
/**
* @file
* Mass import-export and batch import functionality for Gettext .po files.
*/
/**
* Form constructor for the translation import screen.
*
* @see l10n_update_import_form_submit()
*
* @ingroup forms
*/
function l10n_update_import_form($form, &$form_state) {
drupal_static_reset('language_list');
$languages = language_list();
// Initialize a language list to the ones available, including English if we
// are to translate Drupal to English as well.
$existing_languages = array();
foreach ($languages as $langcode => $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;
}
$form_state['redirect_route']['route_name'] = 'locale.translate_page';
}
/**
* 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();
$groups = module_invoke_all('locale', 'groups');
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['textgroup'] = array(
'#type' => 'select',
'#title' => t('Text group'),
'#options' => $groups,
'#default_value' => 'default',
);
$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();
$textgroup = isset($form_state['values']['textgroup']) ? $form_state['values']['textgroup'] : NULL;
$reader = new PoDatabaseReader();
$languageName = '';
if ($language != NULL) {
$reader
->setLangcode($language->id);
$reader
->setOptions($content_options);
$reader
->setTextgroup($textgroup);
$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 bool $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 https://www.drupal.org/node/1191488.
*/
function l10n_update_batch_import_files(array $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 languages.
*
* @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 array $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 array|bool
* A batch structure or FALSE if $files was empty.
*/
function l10n_update_batch_build(array $files, array $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 array $context
* Contains a list of files imported.
*/
function l10n_update_batch_import($file, array $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
// https://www.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 array $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(&$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. <a href="@url">See the log</a> for details.', '@count translation files could not be imported. <a href="@url">See the log</a> 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. <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 {
$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 string $filepath
* The file path of a file to import.
*
* @return object
* 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 languages.
*
* @return bool
* 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;
}
Functions
Name | Description |
---|---|
l10n_update_batch_build | Build a locale batch from an array of files. |
l10n_update_batch_finished | Finished callback of system page locale import batch. |
l10n_update_batch_import | Perform interface translation import as a batch step. |
l10n_update_batch_import_files | Prepare a batch to import all translations. |
l10n_update_batch_import_save | Batch callback: Save data of imported files. |
l10n_update_batch_refresh | Refreshs translations after importing strings. |
l10n_update_delete_translation_files | Deletes interface translation files and translation history records. |
l10n_update_export_form | Form constructor for the Gettext translation files export form. |
l10n_update_export_form_submit | Form submission handler for l10n_update_export_form(). |
l10n_update_file_attach_properties | Generates file properties from filename and options. |
l10n_update_file_create | Creates a file object and populates the timestamp property. |
l10n_update_get_interface_translation_files | Get interface translation files present in the translations directory. |
l10n_update_import_form | Form constructor for the translation import screen. |
l10n_update_import_form_submit | Form submission handler for l10n_update_import_form(). |