annotate core/modules/locale/locale.bulk.inc @ 19:fa3358dc1485 tip

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