l10n_update.bulk.inc 27 KB

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