locale.bulk.inc

Mass import-export and batch import functionality for Gettext .po files.

File

drupal/core/modules/locale/locale.bulk.inc
View source
  1. <?php
  2. /**
  3. * @file
  4. * Mass import-export and batch import functionality for Gettext .po files.
  5. */
  6. use Drupal\Component\Gettext\PoStreamWriter;
  7. use Drupal\locale\Gettext;
  8. use Drupal\locale\PoDatabaseReader;
  9. use Drupal\Core\Language\Language;
  10. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  11. /**
  12. * Form constructor for the translation import screen.
  13. *
  14. * @see locale_translate_import_form_submit()
  15. * @ingroup forms
  16. */
  17. function locale_translate_import_form($form, &$form_state) {
  18. drupal_static_reset('language_list');
  19. $languages = language_list();
  20. // Initialize a language list to the ones available, including English if we
  21. // are to translate Drupal to English as well.
  22. $existing_languages = array();
  23. foreach ($languages as $langcode => $language) {
  24. if ($langcode != 'en' || locale_translate_english()) {
  25. $existing_languages[$langcode] = $language->name;
  26. }
  27. }
  28. // If we have no languages available, present the list of predefined languages
  29. // only. If we do have already added languages, set up two option groups with
  30. // the list of existing and then predefined languages.
  31. form_load_include($form_state, 'inc', 'language', 'language.admin');
  32. if (empty($existing_languages)) {
  33. $language_options = language_admin_predefined_list();
  34. $default = key($language_options);
  35. }
  36. else {
  37. $default = key($existing_languages);
  38. $language_options = array(
  39. t('Existing languages') => $existing_languages,
  40. t('Languages not yet added') => language_admin_predefined_list()
  41. );
  42. }
  43. $validators = array(
  44. 'file_validate_extensions' => array('po'),
  45. 'file_validate_size' => array(file_upload_max_size()),
  46. );
  47. $form['file'] = array(
  48. '#type' => 'file',
  49. '#title' => t('Translation file'),
  50. '#description' => theme('file_upload_help', array('description' => t('A Gettext Portable Object file.'), 'upload_validators' => $validators)),
  51. '#size' => 50,
  52. '#upload_validators' => $validators,
  53. '#attributes' => array('class' => array('file-import-input')),
  54. '#attached' => array(
  55. 'js' => array(
  56. drupal_get_path('module', 'locale') . '/locale.bulk.js' => array(),
  57. ),
  58. ),
  59. );
  60. $form['langcode'] = array(
  61. '#type' => 'select',
  62. '#title' => t('Language'),
  63. '#options' => $language_options,
  64. '#default_value' => $default,
  65. '#attributes' => array('class' => array('langcode-input')),
  66. );
  67. $form['customized'] = array(
  68. '#title' => t('Treat imported strings as custom translations'),
  69. '#type' => 'checkbox',
  70. );
  71. $form['overwrite_options'] = array(
  72. '#type' => 'container',
  73. '#tree' => TRUE,
  74. );
  75. $form['overwrite_options']['not_customized'] = array(
  76. '#title' => t('Overwrite non-customized translations'),
  77. '#type' => 'checkbox',
  78. '#states' => array(
  79. 'checked' => array(
  80. ':input[name="customized"]' => array('checked' => TRUE),
  81. ),
  82. ),
  83. );
  84. $form['overwrite_options']['customized'] = array(
  85. '#title' => t('Overwrite existing customized translations'),
  86. '#type' => 'checkbox',
  87. );
  88. $form['actions'] = array(
  89. '#type' => 'actions'
  90. );
  91. $form['actions']['submit'] = array(
  92. '#type' => 'submit',
  93. '#value' => t('Import')
  94. );
  95. return $form;
  96. }
  97. /**
  98. * Form submission handler for locale_translate_import_form().
  99. */
  100. function locale_translate_import_form_submit($form, &$form_state) {
  101. // Ensure we have the file uploaded.
  102. if ($file = file_save_upload('file', $form['file']['#upload_validators'], 'translations://', 0)) {
  103. // Add language, if not yet supported.
  104. $language = language_load($form_state['values']['langcode']);
  105. if (empty($language)) {
  106. $language = new Language(array(
  107. 'langcode' => $form_state['values']['langcode']
  108. ));
  109. $language = language_save($language);
  110. drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name))));
  111. }
  112. $options = array(
  113. 'langcode' => $form_state['values']['langcode'],
  114. 'overwrite_options' => $form_state['values']['overwrite_options'],
  115. 'customized' => $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED,
  116. );
  117. $file = locale_translate_file_attach_properties($file, $options);
  118. $batch = locale_translate_batch_build(array($file->uri => $file), $options);
  119. batch_set($batch);
  120. }
  121. else {
  122. form_set_error('file', t('File to import not found.'));
  123. $form_state['rebuild'] = TRUE;
  124. return;
  125. }
  126. $form_state['redirect'] = 'admin/config/regional/translate';
  127. return;
  128. }
  129. /**
  130. * Form constructor for the Gettext translation files export form.
  131. *
  132. * @see locale_translate_export_form_submit()
  133. * @ingroup forms
  134. */
  135. function locale_translate_export_form($form, &$form_state) {
  136. $languages = language_list();
  137. $language_options = array();
  138. foreach ($languages as $langcode => $language) {
  139. if ($langcode != 'en' || locale_translate_english()) {
  140. $language_options[$langcode] = $language->name;
  141. }
  142. }
  143. $language_default = language_default();
  144. if (empty($language_options)) {
  145. $form['langcode'] = array(
  146. '#type' => 'value',
  147. '#value' => Language::LANGCODE_SYSTEM,
  148. );
  149. $form['langcode_text'] = array(
  150. '#type' => 'item',
  151. '#title' => t('Language'),
  152. '#markup' => t('No language available. The export will only contain source strings.'),
  153. );
  154. }
  155. else {
  156. $form['langcode'] = array(
  157. '#type' => 'select',
  158. '#title' => t('Language'),
  159. '#options' => $language_options,
  160. '#default_value' => $language_default->langcode,
  161. '#empty_option' => t('Source text only, no translations'),
  162. '#empty_value' => Language::LANGCODE_SYSTEM,
  163. );
  164. $form['content_options'] = array(
  165. '#type' => 'details',
  166. '#title' => t('Export options'),
  167. '#collapsed' => TRUE,
  168. '#tree' => TRUE,
  169. '#states' => array(
  170. 'invisible' => array(
  171. ':input[name="langcode"]' => array('value' => Language::LANGCODE_SYSTEM),
  172. ),
  173. ),
  174. );
  175. $form['content_options']['not_customized'] = array(
  176. '#type' => 'checkbox',
  177. '#title' => t('Include non-customized translations'),
  178. '#default_value' => TRUE,
  179. );
  180. $form['content_options']['customized'] = array(
  181. '#type' => 'checkbox',
  182. '#title' => t('Include customized translations'),
  183. '#default_value' => TRUE,
  184. );
  185. $form['content_options']['not_translated'] = array(
  186. '#type' => 'checkbox',
  187. '#title' => t('Include untranslated text'),
  188. '#default_value' => TRUE,
  189. );
  190. }
  191. $form['actions'] = array(
  192. '#type' => 'actions'
  193. );
  194. $form['actions']['submit'] = array(
  195. '#type' => 'submit',
  196. '#value' => t('Export')
  197. );
  198. return $form;
  199. }
  200. /**
  201. * Form submission handler for locale_translate_export_form().
  202. */
  203. function locale_translate_export_form_submit($form, &$form_state) {
  204. // If template is required, language code is not given.
  205. if ($form_state['values']['langcode'] != Language::LANGCODE_SYSTEM) {
  206. $language = language_load($form_state['values']['langcode']);
  207. }
  208. else {
  209. $language = NULL;
  210. }
  211. $content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
  212. $reader = new PoDatabaseReader();
  213. $languageName = '';
  214. if ($language != NULL) {
  215. $reader->setLangcode($language->langcode);
  216. $reader->setOptions($content_options);
  217. $languages = language_list();
  218. $languageName = isset($languages[$language->langcode]) ? $languages[$language->langcode]->name : '';
  219. $filename = $language->langcode .'.po';
  220. }
  221. else {
  222. // Template required.
  223. $filename = 'drupal.pot';
  224. }
  225. $item = $reader->readItem();
  226. if (!empty($item)) {
  227. $uri = tempnam('temporary://', 'po_');
  228. $header = $reader->getHeader();
  229. $header->setProjectName(config('system.site')->get('name'));
  230. $header->setLanguageName($languageName);
  231. $writer = new PoStreamWriter;
  232. $writer->setUri($uri);
  233. $writer->setHeader($header);
  234. $writer->open();
  235. $writer->writeItem($item);
  236. $writer->writeItems($reader);
  237. $writer->close();
  238. $response = new BinaryFileResponse($uri);
  239. $response->setContentDisposition('attachment', $filename);
  240. // @todo remove lines below once converted to new routing system.
  241. $response->prepare(Drupal::request())
  242. ->send();
  243. }
  244. else {
  245. drupal_set_message('Nothing to export.');
  246. }
  247. }
  248. /**
  249. * Prepare a batch to import all translations.
  250. *
  251. * @param array $options
  252. * An array with options that can have the following elements:
  253. * - 'langcode': The language code. Optional, defaults to NULL, which means
  254. * that the language will be detected from the name of the files.
  255. * - 'overwrite_options': Overwrite options array as defined in
  256. * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
  257. * - 'customized': Flag indicating whether the strings imported from $file
  258. * are customized translations or come from a community source. Use
  259. * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
  260. * LOCALE_NOT_CUSTOMIZED.
  261. * - 'finish_feedback': Whether or not to give feedback to the user when the
  262. * batch is finished. Optional, defaults to TRUE.
  263. *
  264. * @param $force
  265. * (optional) Import all available files, even if they were imported before.
  266. *
  267. * @todo
  268. * Integrate with update status to identify projects needed and integrate
  269. * l10n_update functionality to feed in translation files alike.
  270. * See http://drupal.org/node/1191488.
  271. */
  272. function locale_translate_batch_import_files($options, $force = FALSE) {
  273. $options += array(
  274. 'overwrite_options' => array(),
  275. 'customized' => LOCALE_NOT_CUSTOMIZED,
  276. 'finish_feedback' => TRUE,
  277. );
  278. if (!empty($options['langcode'])) {
  279. $langcodes = array($options['langcode']);
  280. }
  281. else {
  282. // If langcode was not provided, make sure to only import files for the
  283. // languages we have enabled.
  284. $langcodes = array_keys(language_list());
  285. }
  286. $files = locale_translate_get_interface_translation_files(array(), $langcodes);
  287. if (!$force) {
  288. $result = db_select('locale_file', 'lf')
  289. ->fields('lf', array('langcode', 'uri', 'timestamp'))
  290. ->condition('langcode', $langcodes)
  291. ->execute()
  292. ->fetchAllAssoc('uri');
  293. foreach ($result as $uri => $info) {
  294. if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
  295. // The file is already imported and not changed since the last import.
  296. // Remove it from file list and don't import it again.
  297. unset($files[$uri]);
  298. }
  299. }
  300. }
  301. return locale_translate_batch_build($files, $options);
  302. }
  303. /**
  304. * Get interface translation files present in the translations directory.
  305. *
  306. * @param array $projects
  307. * Project names from which to get the translation files and history.
  308. * Defaults to all projects.
  309. * @param array $langcodes
  310. * Language codes from which to get the translation files and history.
  311. * Defaults to all languagues
  312. *
  313. * @return array
  314. * An array of interface translation files keyed by their URI.
  315. */
  316. function locale_translate_get_interface_translation_files($projects = array(), $langcodes = array()) {
  317. module_load_include('compare.inc', 'locale');
  318. $files = array();
  319. $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
  320. $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
  321. // Scan the translations directory for files matching a name pattern
  322. // containing a project name and language code: {project}.{langcode}.po or
  323. // {project}-{version}.{langcode}.po.
  324. // Only files of known projects and languages will be returned.
  325. $directory = config('locale.settings')->get('translation.path');
  326. $result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', array('recurse' => FALSE));
  327. foreach ($result as $file) {
  328. // Update the file object with project name and version from the file name.
  329. $file = locale_translate_file_attach_properties($file);
  330. if (in_array($file->project, $projects)) {
  331. if (in_array($file->langcode, $langcodes)) {
  332. $files[$file->uri] = $file;
  333. }
  334. }
  335. }
  336. return $files;
  337. }
  338. /**
  339. * Build a locale batch from an array of files.
  340. *
  341. * @param $files
  342. * Array of file objects to import.
  343. *
  344. * @param array $options
  345. * An array with options that can have the following elements:
  346. * - 'langcode': The language code. Optional, defaults to NULL, which means
  347. * that the language will be detected from the name of the files.
  348. * - 'overwrite_options': Overwrite options array as defined in
  349. * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
  350. * - 'customized': Flag indicating whether the strings imported from $file
  351. * are customized translations or come from a community source. Use
  352. * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
  353. * LOCALE_NOT_CUSTOMIZED.
  354. * - 'finish_feedback': Whether or not to give feedback to the user when the
  355. * batch is finished. Optional, defaults to TRUE.
  356. *
  357. * @return
  358. * A batch structure or FALSE if $files was empty.
  359. */
  360. function locale_translate_batch_build($files, $options) {
  361. $options += array(
  362. 'overwrite_options' => array(),
  363. 'customized' => LOCALE_NOT_CUSTOMIZED,
  364. 'finish_feedback' => TRUE,
  365. );
  366. $t = get_t();
  367. if (count($files)) {
  368. $operations = array();
  369. foreach ($files as $file) {
  370. // We call locale_translate_batch_import for every batch operation.
  371. $operations[] = array('locale_translate_batch_import', array($file, $options));
  372. }
  373. // Save the translation status of all files.
  374. $operations[] = array('locale_translate_batch_import_save', array());
  375. // Add a final step to refresh JavaScript and configuration strings.
  376. $operations[] = array('locale_translate_batch_refresh', array());
  377. $batch = array(
  378. 'operations' => $operations,
  379. 'title' => $t('Importing interface translations'),
  380. 'progress_message' => '',
  381. 'error_message' => $t('Error importing interface translations'),
  382. 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
  383. );
  384. if ($options['finish_feedback']) {
  385. $batch['finished'] = 'locale_translate_batch_finished';
  386. }
  387. return $batch;
  388. }
  389. return FALSE;
  390. }
  391. /**
  392. * Perform interface translation import as a batch step.
  393. *
  394. * @param object $file
  395. * A file object of the gettext file to be imported. The file object must
  396. * contain a language parameter (other than Language::LANGCODE_NOT_SPECIFIED). This
  397. * is used as the language of the import.
  398. *
  399. * @param array $options
  400. * An array with options that can have the following elements:
  401. * - 'langcode': The language code.
  402. * - 'overwrite_options': Overwrite options array as defined in
  403. * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
  404. * - 'customized': Flag indicating whether the strings imported from $file
  405. * are customized translations or come from a community source. Use
  406. * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
  407. * LOCALE_NOT_CUSTOMIZED.
  408. * - 'message': Alternative message to display during import. Note, this must
  409. * be sanitized text.
  410. *
  411. * @param $context
  412. * Contains a list of files imported.
  413. */
  414. function locale_translate_batch_import($file, $options, &$context) {
  415. // Merge the default values in the $options array.
  416. $options += array(
  417. 'overwrite_options' => array(),
  418. 'customized' => LOCALE_NOT_CUSTOMIZED,
  419. );
  420. if (isset($file->langcode) && $file->langcode != Language::LANGCODE_NOT_SPECIFIED) {
  421. try {
  422. if (empty($context['sandbox'])) {
  423. $context['sandbox']['parse_state'] = array(
  424. 'filesize' => filesize(drupal_realpath($file->uri)),
  425. 'chunk_size' => 200,
  426. 'seek' => 0,
  427. );
  428. }
  429. // Update the seek and the number of items in the $options array().
  430. $options['seek'] = $context['sandbox']['parse_state']['seek'];
  431. $options['items'] = $context['sandbox']['parse_state']['chunk_size'];
  432. $report = GetText::fileToDatabase($file, $options);
  433. // If not yet finished with reading, mark progress based on size and
  434. // position.
  435. if ($report['seek'] < filesize($file->uri)) {
  436. $context['sandbox']['parse_state']['seek'] = $report['seek'];
  437. // Maximize the progress bar at 95% before completion, the batch API
  438. // could trigger the end of the operation before file reading is done,
  439. // because of floating point inaccuracies. See
  440. // http://drupal.org/node/1089472
  441. $context['finished'] = min(0.95, $report['seek'] / filesize($file->uri));
  442. if (isset($options['message'])) {
  443. $context['message'] = t('!message (@percent%).', array('!message' => $options['message'], '@percent' => (int) ($context['finished'] * 100)));
  444. }
  445. else {
  446. $context['message'] = t('Importing translation file: %filename (@percent%).', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)));
  447. }
  448. }
  449. else {
  450. // We are finished here.
  451. $context['finished'] = 1;
  452. // Store the file data for processing by the next batch operation.
  453. $file->timestamp = filemtime($file->uri);
  454. $context['results']['files'][$file->uri] = $file;
  455. $context['results']['languages'][$file->uri] = $file->langcode;
  456. }
  457. // Add the reported values to the statistics for this file.
  458. // Each import iteration reports statistics in an array. The results of
  459. // each iteration are added and merged here and stored per file.
  460. if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) {
  461. $context['results']['stats'][$file->uri] = array();
  462. }
  463. foreach ($report as $key => $value) {
  464. if (is_numeric($report[$key])) {
  465. if (!isset($context['results']['stats'][$file->uri][$key])) {
  466. $context['results']['stats'][$file->uri][$key] = 0;
  467. }
  468. $context['results']['stats'][$file->uri][$key] += $report[$key];
  469. }
  470. elseif (is_array($value)) {
  471. $context['results']['stats'][$file->uri] += array($key => array());
  472. $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value);
  473. }
  474. }
  475. }
  476. catch (Exception $exception) {
  477. // Import failed. Store the data of the failing file.
  478. $context['results']['failed_files'][] = $file;
  479. watchdog('locale', 'Unable to import translations file: @file', array('@file' => $file->uri));
  480. }
  481. }
  482. }
  483. /**
  484. * Batch callback: Save data of imported files.
  485. *
  486. * @param $context
  487. * Contains a list of imported files.
  488. */
  489. function locale_translate_batch_import_save($context) {
  490. if (isset($context['results']['files'])) {
  491. foreach ($context['results']['files'] as $file) {
  492. // Update the file history if both project and version are known. This
  493. // table is used by the automated translation update function which tracks
  494. // translation status of module and themes in the system. Other
  495. // translation files are not tracked and are therefore not stored in this
  496. // table.
  497. if ($file->project && $file->version) {
  498. $file->last_checked = REQUEST_TIME;
  499. locale_translation_update_file_history($file);
  500. }
  501. }
  502. $context['message'] = t('Translations imported.');
  503. }
  504. }
  505. /**
  506. * Refreshs translations after importing strings.
  507. *
  508. * @param array $context
  509. * Contains a list of strings updated and information about the progress.
  510. */
  511. function locale_translate_batch_refresh(array &$context) {
  512. if (!isset($context['sandbox']['refresh'])) {
  513. $strings = $langcodes = array();
  514. if (isset($context['results']['stats'])) {
  515. // Get list of unique string identifiers and language codes updated.
  516. $langcodes = array_unique(array_values($context['results']['languages']));
  517. foreach ($context['results']['stats'] as $filepath => $report) {
  518. $strings = array_merge($strings, $report['strings']);
  519. }
  520. }
  521. if ($strings) {
  522. // Initialize multi-step string refresh.
  523. $context['message'] = t('Updating translations for JavaScript and configuration strings.');
  524. $context['sandbox']['refresh']['strings'] = array_unique($strings);
  525. $context['sandbox']['refresh']['languages'] = $langcodes;
  526. $context['sandbox']['refresh']['names'] = array();
  527. $context['results']['stats']['config'] = 0;
  528. $context['sandbox']['refresh']['count'] = count($strings);
  529. // We will update strings on later steps.
  530. $context['finished'] = 1 - 1 / $context['sandbox']['refresh']['count'];
  531. }
  532. else {
  533. $context['finished'] = 1;
  534. }
  535. }
  536. elseif ($name = array_shift($context['sandbox']['refresh']['names'])) {
  537. // Refresh all languages for one object at a time.
  538. $count = locale_config_update_multiple(array($name), $context['sandbox']['refresh']['languages']);
  539. $context['results']['stats']['config'] += $count;
  540. }
  541. elseif (!empty($context['sandbox']['refresh']['strings'])) {
  542. // Not perfect but will give some indication of progress.
  543. $context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count'];
  544. // Pending strings, refresh 100 at a time, get next pack.
  545. $next = array_slice($context['sandbox']['refresh']['strings'], 0, 100);
  546. array_splice($context['sandbox']['refresh']['strings'], 0, count($next));
  547. // Clear cache and force refresh of JavaScript translations.
  548. _locale_refresh_translations($context['sandbox']['refresh']['languages'], $next);
  549. // Check whether we need to refresh configuration objects.
  550. if ($names = \Drupal\locale\Locale::config()->getStringNames($next)) {
  551. $context['sandbox']['refresh']['names'] = $names;
  552. }
  553. }
  554. else {
  555. $context['finished'] = 1;
  556. }
  557. }
  558. /**
  559. * Finished callback of system page locale import batch.
  560. */
  561. function locale_translate_batch_finished($success, $results) {
  562. if ($success) {
  563. $additions = $updates = $deletes = $skips = $config = 0;
  564. if (isset($results['failed_files'])) {
  565. if (Drupal::moduleHandler()->moduleExists('dblog')) {
  566. $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')));
  567. }
  568. else {
  569. $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.');
  570. }
  571. drupal_set_message($message, 'error');
  572. }
  573. if (isset($results['files'])) {
  574. $skipped_files = array();
  575. // If there are no results and/or no stats (eg. coping with an empty .po
  576. // file), simply do nothing.
  577. if ($results && isset($results['stats'])) {
  578. foreach ($results['stats'] as $filepath => $report) {
  579. $additions += $report['additions'];
  580. $updates += $report['updates'];
  581. $deletes += $report['deletes'];
  582. $skips += $report['skips'];
  583. if ($report['skips'] > 0) {
  584. $skipped_files[] = $filepath;
  585. }
  586. }
  587. }
  588. drupal_set_message(format_plural(count($results['files']),
  589. 'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
  590. '@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.',
  591. array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)
  592. ));
  593. watchdog('locale', 'Translations imported: %number added, %update updated, %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
  594. if ($skips) {
  595. if (module_exists('dblog')) {
  596. $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')));
  597. }
  598. else {
  599. $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.');
  600. }
  601. drupal_set_message($message, 'warning');
  602. watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING);
  603. }
  604. }
  605. }
  606. // Add messages for configuration too.
  607. if (isset($results['stats']['config'])) {
  608. locale_config_batch_finished($success, $results);
  609. }
  610. }
  611. /**
  612. * Creates a file object and populates the timestamp property.
  613. *
  614. * @param $filepath
  615. * The filepath of a file to import.
  616. *
  617. * @return
  618. * An object representing the file.
  619. */
  620. function locale_translate_file_create($filepath) {
  621. $file = new stdClass();
  622. $file->filename = drupal_basename($filepath);
  623. $file->uri = $filepath;
  624. $file->timestamp = filemtime($file->uri);
  625. return $file;
  626. }
  627. /**
  628. * Generates file properties from filename and options.
  629. *
  630. * An attempt is made to determine the translation language, project name and
  631. * project version from the file name. Supported file name patterns are:
  632. * {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po.
  633. * Alternatively the translation language can be set using the $options.
  634. *
  635. * @param object $file
  636. * A file object of the gettext file to be imported.
  637. * @param array $options
  638. * An array with options:
  639. * - 'langcode': The language code. Overrides the file language.
  640. *
  641. * @return object
  642. * Modified file object.
  643. */
  644. function locale_translate_file_attach_properties($file, $options = array()) {
  645. // Extract project, verion and language code from the file name. Supported:
  646. // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po
  647. preg_match('!
  648. ( # project OR project and version OR emtpy (group 1)
  649. ([a-z_]+) # project name (group 2)
  650. \. # .
  651. | # OR
  652. ([a-z_]+) # project name (group 3)
  653. \- # -
  654. ([0-9a-z\.\-\+]+) # version (group 4)
  655. \. # .
  656. | # OR
  657. ) # (empty)
  658. ([^\./]+) # language code (group 5)
  659. \. # .
  660. po # po extension
  661. $!x', $file->filename, $matches);
  662. if (isset($matches[5])) {
  663. $file->project = $matches[2] . $matches[3];
  664. $file->version = $matches[4];
  665. $file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5];
  666. }
  667. else {
  668. $file->langcode = Language::LANGCODE_NOT_SPECIFIED;
  669. }
  670. return $file;
  671. }
  672. /**
  673. * Deletes interface translation files and translation history records.
  674. *
  675. * @param array $projects
  676. * Project names from which to delete the translation files and history.
  677. * Defaults to all projects.
  678. * @param array $langcodes
  679. * Language codes from which to delete the translation files and history.
  680. * Defaults to all languagues
  681. *
  682. * @return boolean
  683. * TRUE if files are removed sucessfully. FALSE if one or more files could
  684. * not be deleted.
  685. */
  686. function locale_translate_delete_translation_files($projects = array(), $langcodes = array()) {
  687. $fail = FALSE;
  688. locale_translation_file_history_delete($projects, $langcodes);
  689. // Delete all translation files from the translations directory.
  690. if ($files = locale_translate_get_interface_translation_files($projects, $langcodes)) {
  691. foreach ($files as $file) {
  692. $success = file_unmanaged_delete($file->uri);
  693. if (!$success) {
  694. $fail = TRUE;
  695. }
  696. }
  697. }
  698. return !$fail;
  699. }
  700. /**
  701. * Builds a locale batch to refresh configuration.
  702. *
  703. * @param array $options
  704. * An array with options that can have the following elements:
  705. * - 'finish_feedback': (optional) Whether or not to give feedback to the user
  706. * when the batch is finished. Defaults to TRUE.
  707. * @param array $langcodes
  708. * (optional) Array of language codes. Defaults to all translatable languages.
  709. * @param array $components
  710. * (optional) Array of component lists indexed by type. If not present or it
  711. * is an empty array, it will update all components.
  712. *
  713. * @return array
  714. * The batch definition.
  715. */
  716. function locale_config_batch_update_components(array $options, $langcodes = array(), $components = array()) {
  717. $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
  718. if ($langcodes && $names = \Drupal\locale\Locale::config()->getComponentNames($components)) {
  719. return locale_config_batch_build($names, $langcodes, $options);
  720. }
  721. }
  722. /**
  723. * Creates a locale batch to refresh specific configuration.
  724. *
  725. * @param array $names
  726. * List of configuration object names (which are strings) to update.
  727. * @param array $langcodes
  728. * List of language codes to refresh.
  729. * @param array $options
  730. * (optional) An array with options that can have the following elements:
  731. * - 'finish_feedback': Whether or not to give feedback to the user when the
  732. * batch is finished. Defaults to TRUE.
  733. *
  734. * @return array
  735. * The batch definition.
  736. *
  737. * @see locale_config_batch_refresh_name()
  738. */
  739. function locale_config_batch_build(array $names, array $langcodes, $options = array()) {
  740. $options += array('finish_feedback' => TRUE);
  741. $t = get_t();
  742. foreach ($names as $name) {
  743. $operations[] = array('locale_config_batch_refresh_name', array($name, $langcodes));
  744. }
  745. $batch = array(
  746. 'operations' => $operations,
  747. 'title' => $t('Updating configuration translations'),
  748. 'init_message' => $t('Starting configuration update'),
  749. 'error_message' => $t('Error updating configuration translations'),
  750. 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
  751. );
  752. if (!empty($options['finish_feedback'])) {
  753. $batch['completed'] = 'locale_config_batch_finished';
  754. }
  755. return $batch;
  756. }
  757. /**
  758. * Performs configuration translation refresh as a batch step.
  759. *
  760. * @param string $name
  761. * Name of configuration object to update.
  762. * @param array $langcodes
  763. * (optional) Array of language codes to update. Defaults to all languages.
  764. * @param array $context
  765. * Contains a list of files imported.
  766. *
  767. * @see locale_config_batch_build()
  768. */
  769. function locale_config_batch_refresh_name($name, array $langcodes, array &$context) {
  770. if (!isset($context['result']['stats']['config'])) {
  771. $context['result']['stats']['config'] = 0;
  772. }
  773. $context['result']['stats']['config'] += locale_config_update_multiple(array($name), $langcodes);
  774. $context['result']['names'][] = $name;
  775. $context['result']['langcodes'] = $langcodes;
  776. $context['finished'] = 1;
  777. }
  778. /**
  779. * Finishes callback of system page locale import batch.
  780. *
  781. * @see locale_config_batch_build()
  782. *
  783. * @param bool $success
  784. * Information about the success of the batch import.
  785. * @param array $results
  786. * Information about the results of the batch import.
  787. */
  788. function locale_config_batch_finished($success, array $results) {
  789. if ($success) {
  790. $configuration = isset($results['stats']['config']) ? $results['stats']['config'] : 0;
  791. if ($configuration) {
  792. drupal_set_message(t('The configuration was successfully updated. There are %number configuration objects updated.', array('%number' => $configuration)));
  793. watchdog('locale', 'The configuration was successfully updated. %number configuration objects updated.', array('%number' => $configuration));
  794. }
  795. else {
  796. drupal_set_message(t('No configuration objects have been updated.'));
  797. watchdog('locale', 'No configuration objects have been updated.', array(), WATCHDOG_WARNING);
  798. }
  799. }
  800. }
  801. /**
  802. * Updates all configuration for names / languages.
  803. *
  804. * @param array $names
  805. * Array of names of configuration objects to update.
  806. * @param array $langcodes
  807. * (optional) Array of language codes to update. Defaults to all languages.
  808. *
  809. * @return int
  810. * Number of configuration objects retranslated.
  811. */
  812. function locale_config_update_multiple(array $names, $langcodes = array()) {
  813. $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
  814. $count = 0;
  815. foreach ($names as $name) {
  816. $wrapper = \Drupal\locale\Locale::config()->get($name);
  817. foreach ($langcodes as $langcode) {
  818. $translation = $wrapper->getValue() ? $wrapper->getTranslation($langcode)->getValue() : NULL;
  819. if ($translation) {
  820. \Drupal\locale\Locale::config()->saveTranslationData($name, $langcode, $translation);
  821. $count++;
  822. }
  823. else {
  824. \Drupal\locale\Locale::config()->deleteTranslationData($name, $langcode);
  825. }
  826. }
  827. }
  828. return $count;
  829. }

Functions

Namesort descending Description
locale_config_batch_build Creates a locale batch to refresh specific configuration.
locale_config_batch_finished Finishes callback of system page locale import batch.
locale_config_batch_refresh_name Performs configuration translation refresh as a batch step.
locale_config_batch_update_components Builds a locale batch to refresh configuration.
locale_config_update_multiple Updates all configuration for names / languages.
locale_translate_batch_build Build a locale batch from an array of files.
locale_translate_batch_finished Finished callback of system page locale import batch.
locale_translate_batch_import Perform interface translation import as a batch step.
locale_translate_batch_import_files Prepare a batch to import all translations.
locale_translate_batch_import_save Batch callback: Save data of imported files.
locale_translate_batch_refresh Refreshs translations after importing strings.
locale_translate_delete_translation_files Deletes interface translation files and translation history records.
locale_translate_export_form Form constructor for the Gettext translation files export form.
locale_translate_export_form_submit Form submission handler for locale_translate_export_form().
locale_translate_file_attach_properties Generates file properties from filename and options.
locale_translate_file_create Creates a file object and populates the timestamp property.
locale_translate_get_interface_translation_files Get interface translation files present in the translations directory.
locale_translate_import_form Form constructor for the translation import screen.
locale_translate_import_form_submit Form submission handler for locale_translate_import_form().