l10n_update.bulk.inc 27 KB

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