locale.bulk.inc 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. <?php
  2. /**
  3. * @file
  4. * Mass import-export and batch import functionality for Gettext .po files.
  5. */
  6. use Drupal\Core\Url;
  7. use Drupal\Core\File\Exception\FileException;
  8. use Drupal\Core\Language\LanguageInterface;
  9. use Drupal\file\FileInterface;
  10. use Drupal\locale\Gettext;
  11. use Drupal\locale\Locale;
  12. /**
  13. * Prepare a batch to import all translations.
  14. *
  15. * @param array $options
  16. * An array with options that can have the following elements:
  17. * - 'langcode': The language code. Optional, defaults to NULL, which means
  18. * that the language will be detected from the name of the files.
  19. * - 'overwrite_options': Overwrite options array as defined in
  20. * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
  21. * - 'customized': Flag indicating whether the strings imported from $file
  22. * are customized translations or come from a community source. Use
  23. * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
  24. * LOCALE_NOT_CUSTOMIZED.
  25. * - 'finish_feedback': Whether or not to give feedback to the user when the
  26. * batch is finished. Optional, defaults to TRUE.
  27. * @param bool $force
  28. * (optional) Import all available files, even if they were imported before.
  29. *
  30. * @return array|bool
  31. * The batch structure, or FALSE if no files are used to build the batch.
  32. *
  33. * @todo
  34. * Integrate with update status to identify projects needed and integrate
  35. * l10n_update functionality to feed in translation files alike.
  36. * See https://www.drupal.org/node/1191488.
  37. */
  38. function locale_translate_batch_import_files(array $options, $force = FALSE) {
  39. $options += [
  40. 'overwrite_options' => [],
  41. 'customized' => LOCALE_NOT_CUSTOMIZED,
  42. 'finish_feedback' => TRUE,
  43. ];
  44. if (!empty($options['langcode'])) {
  45. $langcodes = [$options['langcode']];
  46. }
  47. else {
  48. // If langcode was not provided, make sure to only import files for the
  49. // languages we have added.
  50. $langcodes = array_keys(\Drupal::languageManager()->getLanguages());
  51. }
  52. $files = locale_translate_get_interface_translation_files([], $langcodes);
  53. if (!$force) {
  54. $result = \Drupal::database()->select('locale_file', 'lf')
  55. ->fields('lf', ['langcode', 'uri', 'timestamp'])
  56. ->condition('langcode', $langcodes)
  57. ->execute()
  58. ->fetchAllAssoc('uri');
  59. foreach ($result as $uri => $info) {
  60. if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
  61. // The file is already imported and not changed since the last import.
  62. // Remove it from file list and don't import it again.
  63. unset($files[$uri]);
  64. }
  65. }
  66. }
  67. return locale_translate_batch_build($files, $options);
  68. }
  69. /**
  70. * Get interface translation files present in the translations directory.
  71. *
  72. * @param array $projects
  73. * (optional) Project names from which to get the translation files and
  74. * history. Defaults to all projects.
  75. * @param array $langcodes
  76. * (optional) Language codes from which to get the translation files and
  77. * history. Defaults to all languages.
  78. *
  79. * @return array
  80. * An array of interface translation files keyed by their URI.
  81. */
  82. function locale_translate_get_interface_translation_files(array $projects = [], array $langcodes = []) {
  83. module_load_include('compare.inc', 'locale');
  84. $files = [];
  85. $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
  86. $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
  87. // Scan the translations directory for files matching a name pattern
  88. // containing a project name and language code: {project}.{langcode}.po or
  89. // {project}-{version}.{langcode}.po.
  90. // Only files of known projects and languages will be returned.
  91. $directory = \Drupal::config('locale.settings')->get('translation.path');
  92. $result = [];
  93. if (is_dir($directory)) {
  94. $result = \Drupal::service('file_system')->scanDirectory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', ['recurse' => FALSE]);
  95. }
  96. foreach ($result as $file) {
  97. // Update the file object with project name and version from the file name.
  98. $file = locale_translate_file_attach_properties($file);
  99. if (in_array($file->project, $projects)) {
  100. if (in_array($file->langcode, $langcodes)) {
  101. $files[$file->uri] = $file;
  102. }
  103. }
  104. }
  105. return $files;
  106. }
  107. /**
  108. * Build a locale batch from an array of files.
  109. *
  110. * @param array $files
  111. * Array of file objects to import.
  112. * @param array $options
  113. * An array with options that can have the following elements:
  114. * - 'langcode': The language code. Optional, defaults to NULL, which means
  115. * that the language will be detected from the name of the files.
  116. * - 'overwrite_options': Overwrite options array as defined in
  117. * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
  118. * - 'customized': Flag indicating whether the strings imported from $file
  119. * are customized translations or come from a community source. Use
  120. * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
  121. * LOCALE_NOT_CUSTOMIZED.
  122. * - 'finish_feedback': Whether or not to give feedback to the user when the
  123. * batch is finished. Optional, defaults to TRUE.
  124. *
  125. * @return array|bool
  126. * A batch structure or FALSE if $files was empty.
  127. */
  128. function locale_translate_batch_build(array $files, array $options) {
  129. $options += [
  130. 'overwrite_options' => [],
  131. 'customized' => LOCALE_NOT_CUSTOMIZED,
  132. 'finish_feedback' => TRUE,
  133. ];
  134. if (count($files)) {
  135. $operations = [];
  136. foreach ($files as $file) {
  137. // We call locale_translate_batch_import for every batch operation.
  138. $operations[] = ['locale_translate_batch_import', [$file, $options]];
  139. }
  140. // Save the translation status of all files.
  141. $operations[] = ['locale_translate_batch_import_save', []];
  142. // Add a final step to refresh JavaScript and configuration strings.
  143. $operations[] = ['locale_translate_batch_refresh', []];
  144. $batch = [
  145. 'operations' => $operations,
  146. 'title' => t('Importing interface translations'),
  147. 'progress_message' => '',
  148. 'error_message' => t('Error importing interface translations'),
  149. 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
  150. ];
  151. if ($options['finish_feedback']) {
  152. $batch['finished'] = 'locale_translate_batch_finished';
  153. }
  154. return $batch;
  155. }
  156. return FALSE;
  157. }
  158. /**
  159. * Implements callback_batch_operation().
  160. *
  161. * Perform interface translation import.
  162. *
  163. * @param object $file
  164. * A file object of the gettext file to be imported. The file object must
  165. * contain a language parameter (other than
  166. * LanguageInterface::LANGCODE_NOT_SPECIFIED). This is used as the language of
  167. * the import.
  168. * @param array $options
  169. * An array with options that can have the following elements:
  170. * - 'langcode': The language code.
  171. * - 'overwrite_options': Overwrite options array as defined in
  172. * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
  173. * - 'customized': Flag indicating whether the strings imported from $file
  174. * are customized translations or come from a community source. Use
  175. * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
  176. * LOCALE_NOT_CUSTOMIZED.
  177. * - 'message': Alternative message to display during import. Note, this must
  178. * be sanitized text.
  179. * @param array|\ArrayAccess $context
  180. * Contains a list of files imported.
  181. */
  182. function locale_translate_batch_import($file, array $options, &$context) {
  183. // Merge the default values in the $options array.
  184. $options += [
  185. 'overwrite_options' => [],
  186. 'customized' => LOCALE_NOT_CUSTOMIZED,
  187. ];
  188. if (isset($file->langcode) && $file->langcode != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
  189. try {
  190. if (empty($context['sandbox'])) {
  191. $context['sandbox']['parse_state'] = [
  192. 'filesize' => filesize(\Drupal::service('file_system')->realpath($file->uri)),
  193. 'chunk_size' => 200,
  194. 'seek' => 0,
  195. ];
  196. }
  197. // Update the seek and the number of items in the $options array().
  198. $options['seek'] = $context['sandbox']['parse_state']['seek'];
  199. $options['items'] = $context['sandbox']['parse_state']['chunk_size'];
  200. $report = Gettext::fileToDatabase($file, $options);
  201. // If not yet finished with reading, mark progress based on size and
  202. // position.
  203. if ($report['seek'] < filesize($file->uri)) {
  204. $context['sandbox']['parse_state']['seek'] = $report['seek'];
  205. // Maximize the progress bar at 95% before completion, the batch API
  206. // could trigger the end of the operation before file reading is done,
  207. // because of floating point inaccuracies. See
  208. // https://www.drupal.org/node/1089472.
  209. $context['finished'] = min(0.95, $report['seek'] / filesize($file->uri));
  210. if (isset($options['message'])) {
  211. $context['message'] = t('@message (@percent%).', ['@message' => $options['message'], '@percent' => (int) ($context['finished'] * 100)]);
  212. }
  213. else {
  214. $context['message'] = t('Importing translation file: %filename (@percent%).', ['%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)]);
  215. }
  216. }
  217. else {
  218. // We are finished here.
  219. $context['finished'] = 1;
  220. // Store the file data for processing by the next batch operation.
  221. $file->timestamp = filemtime($file->uri);
  222. $context['results']['files'][$file->uri] = $file;
  223. $context['results']['languages'][$file->uri] = $file->langcode;
  224. }
  225. // Add the reported values to the statistics for this file.
  226. // Each import iteration reports statistics in an array. The results of
  227. // each iteration are added and merged here and stored per file.
  228. if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) {
  229. $context['results']['stats'][$file->uri] = [];
  230. }
  231. foreach ($report as $key => $value) {
  232. if (is_numeric($report[$key])) {
  233. if (!isset($context['results']['stats'][$file->uri][$key])) {
  234. $context['results']['stats'][$file->uri][$key] = 0;
  235. }
  236. $context['results']['stats'][$file->uri][$key] += $report[$key];
  237. }
  238. elseif (is_array($value)) {
  239. $context['results']['stats'][$file->uri] += [$key => []];
  240. $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value);
  241. }
  242. }
  243. }
  244. catch (Exception $exception) {
  245. // Import failed. Store the data of the failing file.
  246. $context['results']['failed_files'][] = $file;
  247. \Drupal::logger('locale')->notice('Unable to import translations file: @file', ['@file' => $file->uri]);
  248. }
  249. }
  250. }
  251. /**
  252. * Implements callback_batch_operation().
  253. *
  254. * Save data of imported files.
  255. *
  256. * @param array|\ArrayAccess $context
  257. * Contains a list of imported files.
  258. */
  259. function locale_translate_batch_import_save($context) {
  260. if (isset($context['results']['files'])) {
  261. foreach ($context['results']['files'] as $file) {
  262. // Update the file history if both project and version are known. This
  263. // table is used by the automated translation update function which tracks
  264. // translation status of module and themes in the system. Other
  265. // translation files are not tracked and are therefore not stored in this
  266. // table.
  267. if ($file->project && $file->version) {
  268. $file->last_checked = REQUEST_TIME;
  269. locale_translation_update_file_history($file);
  270. }
  271. }
  272. $context['message'] = t('Translations imported.');
  273. }
  274. }
  275. /**
  276. * Implements callback_batch_operation().
  277. *
  278. * Refreshes translations after importing strings.
  279. *
  280. * @param array|\ArrayAccess $context
  281. * Contains a list of strings updated and information about the progress.
  282. */
  283. function locale_translate_batch_refresh(&$context) {
  284. if (!isset($context['sandbox']['refresh'])) {
  285. $strings = $langcodes = [];
  286. if (isset($context['results']['stats'])) {
  287. // Get list of unique string identifiers and language codes updated.
  288. $langcodes = array_unique(array_values($context['results']['languages']));
  289. foreach ($context['results']['stats'] as $report) {
  290. $strings = array_merge($strings, $report['strings']);
  291. }
  292. }
  293. if ($strings) {
  294. // Initialize multi-step string refresh.
  295. $context['message'] = t('Updating translations for JavaScript and default configuration.');
  296. $context['sandbox']['refresh']['strings'] = array_unique($strings);
  297. $context['sandbox']['refresh']['languages'] = $langcodes;
  298. $context['sandbox']['refresh']['names'] = [];
  299. $context['results']['stats']['config'] = 0;
  300. $context['sandbox']['refresh']['count'] = count($strings);
  301. // We will update strings on later steps.
  302. $context['finished'] = 0;
  303. }
  304. else {
  305. $context['finished'] = 1;
  306. }
  307. }
  308. elseif ($name = array_shift($context['sandbox']['refresh']['names'])) {
  309. // Refresh all languages for one object at a time.
  310. $count = Locale::config()->updateConfigTranslations([$name], $context['sandbox']['refresh']['languages']);
  311. $context['results']['stats']['config'] += $count;
  312. // Inherit finished information from the "parent" string lookup step so
  313. // visual display of status will make sense.
  314. $context['finished'] = $context['sandbox']['refresh']['names_finished'];
  315. $context['message'] = t('Updating default configuration (@percent%).', ['@percent' => (int) ($context['finished'] * 100)]);
  316. }
  317. elseif (!empty($context['sandbox']['refresh']['strings'])) {
  318. // Not perfect but will give some indication of progress.
  319. $context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count'];
  320. // Pending strings, refresh 100 at a time, get next pack.
  321. $next = array_slice($context['sandbox']['refresh']['strings'], 0, 100);
  322. array_splice($context['sandbox']['refresh']['strings'], 0, count($next));
  323. // Clear cache and force refresh of JavaScript translations.
  324. _locale_refresh_translations($context['sandbox']['refresh']['languages'], $next);
  325. // Check whether we need to refresh configuration objects.
  326. if ($names = Locale::config()->getStringNames($next)) {
  327. $context['sandbox']['refresh']['names_finished'] = $context['finished'];
  328. $context['sandbox']['refresh']['names'] = $names;
  329. }
  330. }
  331. else {
  332. $context['message'] = t('Updated default configuration.');
  333. $context['finished'] = 1;
  334. }
  335. }
  336. /**
  337. * Implements callback_batch_finished().
  338. *
  339. * Finished callback of system page locale import batch.
  340. *
  341. * @param bool $success
  342. * TRUE if batch successfully completed.
  343. * @param array $results
  344. * Batch results.
  345. */
  346. function locale_translate_batch_finished($success, array $results) {
  347. $logger = \Drupal::logger('locale');
  348. if ($success) {
  349. $additions = $updates = $deletes = $skips = $config = 0;
  350. if (isset($results['failed_files'])) {
  351. if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
  352. $message = \Drupal::translation()->formatPlural(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.', [':url' => Url::fromRoute('dblog.overview')->toString()]);
  353. }
  354. else {
  355. $message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.');
  356. }
  357. \Drupal::messenger()->addError($message);
  358. }
  359. if (isset($results['files'])) {
  360. $skipped_files = [];
  361. // If there are no results and/or no stats (eg. coping with an empty .po
  362. // file), simply do nothing.
  363. if ($results && isset($results['stats'])) {
  364. foreach ($results['stats'] as $filepath => $report) {
  365. if ($filepath === 'config') {
  366. // Ignore the config entry. It is processed in
  367. // locale_config_batch_finished() below.
  368. continue;
  369. }
  370. $additions += $report['additions'];
  371. $updates += $report['updates'];
  372. $deletes += $report['deletes'];
  373. $skips += $report['skips'];
  374. if ($report['skips'] > 0) {
  375. $skipped_files[] = $filepath;
  376. }
  377. }
  378. }
  379. \Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural(count($results['files']),
  380. 'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
  381. '@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.',
  382. ['%number' => $additions, '%update' => $updates, '%delete' => $deletes]
  383. ));
  384. $logger->notice('Translations imported: %number added, %update updated, %delete removed.', ['%number' => $additions, '%update' => $updates, '%delete' => $deletes]);
  385. if ($skips) {
  386. if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
  387. $message = \Drupal::translation()->formatPlural($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.', [':url' => Url::fromRoute('dblog.overview')->toString()]);
  388. }
  389. else {
  390. $message = \Drupal::translation()->formatPlural($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.');
  391. }
  392. \Drupal::messenger()->addWarning($message);
  393. $logger->warning('@count disallowed HTML string(s) in files: @files.', ['@count' => $skips, '@files' => implode(',', $skipped_files)]);
  394. }
  395. }
  396. }
  397. // Add messages for configuration too.
  398. if (isset($results['stats']['config'])) {
  399. locale_config_batch_finished($success, $results);
  400. }
  401. }
  402. /**
  403. * Creates a file object and populates the timestamp property.
  404. *
  405. * @param string $filepath
  406. * The filepath of a file to import.
  407. *
  408. * @return object
  409. * An object representing the file.
  410. */
  411. function locale_translate_file_create($filepath) {
  412. $file = new stdClass();
  413. $file->filename = \Drupal::service('file_system')->basename($filepath);
  414. $file->uri = $filepath;
  415. $file->timestamp = filemtime($file->uri);
  416. return $file;
  417. }
  418. /**
  419. * Generates file properties from filename and options.
  420. *
  421. * An attempt is made to determine the translation language, project name and
  422. * project version from the file name. Supported file name patterns are:
  423. * {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po.
  424. * Alternatively the translation language can be set using the $options.
  425. *
  426. * @param object $file
  427. * A file object of the gettext file to be imported.
  428. * @param array $options
  429. * An array with options:
  430. * - 'langcode': The language code. Overrides the file language.
  431. *
  432. * @return object
  433. * Modified file object.
  434. */
  435. function locale_translate_file_attach_properties($file, array $options = []) {
  436. // If $file is a file entity, convert it to a stdClass.
  437. if ($file instanceof FileInterface) {
  438. $file = (object) [
  439. 'filename' => $file->getFilename(),
  440. 'uri' => $file->getFileUri(),
  441. ];
  442. }
  443. // Extract project, version and language code from the file name. Supported:
  444. // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po
  445. preg_match('!
  446. ( # project OR project and version OR empty (group 1)
  447. ([a-z_]+) # project name (group 2)
  448. \. # .
  449. | # OR
  450. ([a-z_]+) # project name (group 3)
  451. \- # -
  452. ([0-9a-z\.\-\+]+) # version (group 4)
  453. \. # .
  454. | # OR
  455. ) # (empty)
  456. ([^\./]+) # language code (group 5)
  457. \. # .
  458. po # po extension
  459. $!x', $file->filename, $matches);
  460. if (isset($matches[5])) {
  461. $file->project = $matches[2] . $matches[3];
  462. $file->version = $matches[4];
  463. $file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5];
  464. }
  465. else {
  466. $file->langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
  467. }
  468. return $file;
  469. }
  470. /**
  471. * Deletes interface translation files and translation history records.
  472. *
  473. * @param array $projects
  474. * (optional) Project names from which to delete the translation files and
  475. * history. Defaults to all projects.
  476. * @param array $langcodes
  477. * (optional) Language codes from which to delete the translation files and
  478. * history. Defaults to all languages.
  479. *
  480. * @return bool
  481. * TRUE if files are removed successfully. FALSE if one or more files could
  482. * not be deleted.
  483. */
  484. function locale_translate_delete_translation_files(array $projects = [], array $langcodes = []) {
  485. $fail = FALSE;
  486. locale_translation_file_history_delete($projects, $langcodes);
  487. // Delete all translation files from the translations directory.
  488. if ($files = locale_translate_get_interface_translation_files($projects, $langcodes)) {
  489. foreach ($files as $file) {
  490. try {
  491. \Drupal::service('file_system')->delete($file->uri);
  492. }
  493. catch (FileException $e) {
  494. $fail = TRUE;
  495. }
  496. }
  497. }
  498. return !$fail;
  499. }
  500. /**
  501. * Builds a locale batch to refresh configuration.
  502. *
  503. * @param array $options
  504. * An array with options that can have the following elements:
  505. * - 'finish_feedback': (optional) Whether or not to give feedback to the user
  506. * when the batch is finished. Defaults to TRUE.
  507. * @param array $langcodes
  508. * (optional) Array of language codes. Defaults to all translatable languages.
  509. * @param array $components
  510. * (optional) Array of component lists indexed by type. If not present or it
  511. * is an empty array, it will update all components.
  512. *
  513. * @return array
  514. * The batch definition.
  515. */
  516. function locale_config_batch_update_components(array $options, array $langcodes = [], array $components = []) {
  517. $langcodes = $langcodes ? $langcodes : array_keys(\Drupal::languageManager()->getLanguages());
  518. if ($langcodes && $names = Locale::config()->getComponentNames($components)) {
  519. return locale_config_batch_build($names, $langcodes, $options);
  520. }
  521. }
  522. /**
  523. * Creates a locale batch to refresh specific configuration.
  524. *
  525. * @param array $names
  526. * List of configuration object names (which are strings) to update.
  527. * @param array $langcodes
  528. * List of language codes to refresh.
  529. * @param array $options
  530. * (optional) An array with options that can have the following elements:
  531. * - 'finish_feedback': Whether or not to give feedback to the user when the
  532. * batch is finished. Defaults to TRUE.
  533. *
  534. * @return array
  535. * The batch definition.
  536. *
  537. * @see locale_config_batch_refresh_name()
  538. */
  539. function locale_config_batch_build(array $names, array $langcodes, array $options = []) {
  540. $options += ['finish_feedback' => TRUE];
  541. $i = 0;
  542. $batch_names = [];
  543. $operations = [];
  544. foreach ($names as $name) {
  545. $batch_names[] = $name;
  546. $i++;
  547. // During installation the caching of configuration objects is disabled so
  548. // it is very expensive to initialize the \Drupal::config() object on each
  549. // request. We batch a small number of configuration object upgrades
  550. // together to improve the overall performance of the process.
  551. if ($i % 20 == 0) {
  552. $operations[] = ['locale_config_batch_refresh_name', [$batch_names, $langcodes]];
  553. $batch_names = [];
  554. }
  555. }
  556. if (!empty($batch_names)) {
  557. $operations[] = ['locale_config_batch_refresh_name', [$batch_names, $langcodes]];
  558. }
  559. $batch = [
  560. 'operations' => $operations,
  561. 'title' => t('Updating configuration translations'),
  562. 'init_message' => t('Starting configuration update'),
  563. 'error_message' => t('Error updating configuration translations'),
  564. 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
  565. ];
  566. if (!empty($options['finish_feedback'])) {
  567. $batch['completed'] = 'locale_config_batch_finished';
  568. }
  569. return $batch;
  570. }
  571. /**
  572. * Implements callback_batch_operation().
  573. *
  574. * Performs configuration translation refresh.
  575. *
  576. * @param array $names
  577. * An array of names of configuration objects to update.
  578. * @param array $langcodes
  579. * (optional) Array of language codes to update. Defaults to all languages.
  580. * @param array|\ArrayAccess $context
  581. * Contains a list of files imported.
  582. *
  583. * @see locale_config_batch_build()
  584. */
  585. function locale_config_batch_refresh_name(array $names, array $langcodes, &$context) {
  586. if (!isset($context['result']['stats']['config'])) {
  587. $context['result']['stats']['config'] = 0;
  588. }
  589. $context['result']['stats']['config'] += Locale::config()->updateConfigTranslations($names, $langcodes);
  590. foreach ($names as $name) {
  591. $context['result']['names'][] = $name;
  592. }
  593. $context['result']['langcodes'] = $langcodes;
  594. $context['finished'] = 1;
  595. }
  596. /**
  597. * Implements callback_batch_finished().
  598. *
  599. * Finishes callback of system page locale import batch.
  600. *
  601. * @param bool $success
  602. * Information about the success of the batch import.
  603. * @param array $results
  604. * Information about the results of the batch import.
  605. *
  606. * @see locale_config_batch_build()
  607. */
  608. function locale_config_batch_finished($success, array $results) {
  609. if ($success) {
  610. $configuration = isset($results['stats']['config']) ? $results['stats']['config'] : 0;
  611. if ($configuration) {
  612. \Drupal::messenger()->addStatus(t('The configuration was successfully updated. There are %number configuration objects updated.', ['%number' => $configuration]));
  613. \Drupal::logger('locale')->notice('The configuration was successfully updated. %number configuration objects updated.', ['%number' => $configuration]);
  614. }
  615. else {
  616. \Drupal::messenger()->addStatus(t('No configuration objects have been updated.'));
  617. \Drupal::logger('locale')->warning('No configuration objects have been updated.');
  618. }
  619. }
  620. }