Chris@0: TRUE,
Chris@0: 'use_remote' => TRUE,
Chris@0: ];
Chris@0: $source = locale_translation_get_status([$project], [$langcode]);
Chris@0: $source = $source[$project][$langcode];
Chris@0:
Chris@0: // Check the status of local translation files.
Chris@0: if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
Chris@0: if ($file = locale_translation_source_check_file($source)) {
Chris@0: locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
Chris@0: }
Chris@0: $checked = TRUE;
Chris@0: }
Chris@0:
Chris@0: // Check the status of remote translation files.
Chris@0: if ($options['use_remote'] && isset($source->files[LOCALE_TRANSLATION_REMOTE])) {
Chris@0: $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE];
Chris@0: if ($result = locale_translation_http_check($remote_file->uri)) {
Chris@0: // Update the file object with the result data. In case of a redirect we
Chris@0: // store the resulting uri.
Chris@0: if (isset($result['last_modified'])) {
Chris@0: $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri;
Chris@0: $remote_file->timestamp = $result['last_modified'];
Chris@0: locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_REMOTE, $remote_file);
Chris@0: }
Chris@0: // @todo What to do with when the file is not found (404)? To prevent
Chris@0: // re-checking within the TTL (1day, 1week) we can set a last_checked
Chris@0: // timestamp or cache the result.
Chris@0: $checked = TRUE;
Chris@0: }
Chris@0: else {
Chris@0: $failure = TRUE;
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: // Provide user feedback and record success or failure for reporting at the
Chris@0: // end of the batch.
Chris@0: if ($options['finish_feedback'] && $checked) {
Chris@0: $context['results']['files'][] = $source->name;
Chris@0: }
Chris@0: if ($failure && !$checked) {
Chris@0: $context['results']['failed_files'][] = $source->name;
Chris@0: }
Chris@0: $context['message'] = t('Checked translation for %project.', ['%project' => $source->project]);
Chris@0: }
Chris@0:
Chris@0: /**
Chris@0: * Implements callback_batch_finished().
Chris@0: *
Chris@0: * Set result message.
Chris@0: *
Chris@0: * @param bool $success
Chris@0: * TRUE if batch successfully completed.
Chris@0: * @param array $results
Chris@0: * Batch results.
Chris@0: */
Chris@0: function locale_translation_batch_status_finished($success, $results) {
Chris@0: if ($success) {
Chris@0: if (isset($results['failed_files'])) {
Chris@0: if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
Chris@18: $message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.', [':url' => Url::fromRoute('dblog.overview')->toString()]);
Chris@0: }
Chris@0: else {
Chris@0: $message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation files could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.');
Chris@0: }
Chris@17: \Drupal::messenger()->addError($message);
Chris@0: }
Chris@0: if (isset($results['files'])) {
Chris@17: \Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural(
Chris@0: count($results['files']),
Chris@0: 'Checked available interface translation updates for one project.',
Chris@0: 'Checked available interface translation updates for @count projects.'
Chris@0: ));
Chris@0: }
Chris@0: if (!isset($results['failed_files']) && !isset($results['files'])) {
Chris@17: \Drupal::messenger()->addStatus(t('Nothing to check.'));
Chris@0: }
Chris@0: \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
Chris@0: }
Chris@0: else {
Chris@17: \Drupal::messenger()->addError(t('An error occurred trying to check available interface translation updates.'));
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: /**
Chris@0: * Implements callback_batch_operation().
Chris@0: *
Chris@0: * Downloads a remote gettext file into the translations directory. When
Chris@0: * successfully the translation status is updated.
Chris@0: *
Chris@0: * @param object $project
Chris@0: * Source object of the translatable project.
Chris@0: * @param string $langcode
Chris@0: * Language code.
Chris@0: * @param array $context
Chris@0: * The batch context.
Chris@0: *
Chris@0: * @see locale_translation_batch_fetch_import()
Chris@0: */
Chris@0: function locale_translation_batch_fetch_download($project, $langcode, &$context) {
Chris@0: $sources = locale_translation_get_status([$project], [$langcode]);
Chris@0: if (isset($sources[$project][$langcode])) {
Chris@0: $source = $sources[$project][$langcode];
Chris@0: if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) {
Chris@0: if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE], 'translations://')) {
Chris@0: $context['message'] = t('Downloaded translation for %project.', ['%project' => $source->project]);
Chris@0: locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
Chris@0: }
Chris@0: else {
Chris@0: $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE];
Chris@0: }
Chris@0: }
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: /**
Chris@0: * Implements callback_batch_operation().
Chris@0: *
Chris@0: * Imports a gettext file from the translation directory. When successfully the
Chris@0: * translation status is updated.
Chris@0: *
Chris@0: * @param object $project
Chris@0: * Source object of the translatable project.
Chris@0: * @param string $langcode
Chris@0: * Language code.
Chris@0: * @param array $options
Chris@0: * Array of import options.
Chris@0: * @param array $context
Chris@0: * The batch context.
Chris@0: *
Chris@0: * @see locale_translate_batch_import_files()
Chris@0: * @see locale_translation_batch_fetch_download()
Chris@0: */
Chris@0: function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) {
Chris@0: $sources = locale_translation_get_status([$project], [$langcode]);
Chris@0: if (isset($sources[$project][$langcode])) {
Chris@0: $source = $sources[$project][$langcode];
Chris@0: if (isset($source->type)) {
Chris@0: if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) {
Chris@0: $file = $source->files[LOCALE_TRANSLATION_LOCAL];
Chris@0: module_load_include('bulk.inc', 'locale');
Chris@0: $options += [
Chris@0: 'message' => t('Importing translation for %project.', ['%project' => $source->project]),
Chris@0: ];
Chris@0: // Import the translation file. For large files the batch operations is
Chris@0: // progressive and will be called repeatedly until finished.
Chris@0: locale_translate_batch_import($file, $options, $context);
Chris@0:
Chris@0: // The import is finished.
Chris@0: if (isset($context['finished']) && $context['finished'] == 1) {
Chris@0: // The import is successful.
Chris@0: if (isset($context['results']['files'][$file->uri])) {
Chris@0: $context['message'] = t('Imported translation for %project.', ['%project' => $source->project]);
Chris@0:
Chris@0: // Save the data of imported source into the {locale_file} table and
Chris@0: // update the current translation status.
Chris@0: locale_translation_status_save($project, $langcode, LOCALE_TRANSLATION_CURRENT, $source->files[LOCALE_TRANSLATION_LOCAL]);
Chris@0: }
Chris@0: }
Chris@0: }
Chris@0: }
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: /**
Chris@0: * Implements callback_batch_finished().
Chris@0: *
Chris@0: * Set result message.
Chris@0: *
Chris@0: * @param bool $success
Chris@0: * TRUE if batch successfully completed.
Chris@0: * @param array $results
Chris@0: * Batch results.
Chris@0: */
Chris@0: function locale_translation_batch_fetch_finished($success, $results) {
Chris@0: module_load_include('bulk.inc', 'locale');
Chris@0: if ($success) {
Chris@0: \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
Chris@0: }
Chris@0: return locale_translate_batch_finished($success, $results);
Chris@0: }
Chris@0:
Chris@0: /**
Chris@0: * Check if remote file exists and when it was last updated.
Chris@0: *
Chris@0: * @param string $uri
Chris@0: * URI of remote file.
Chris@0: *
Chris@0: * @return array|bool
Chris@0: * Associative array of file data with the following elements:
Chris@0: * - last_modified: Last modified timestamp of the translation file.
Chris@0: * - (optional) location: The location of the translation file. Is only set
Chris@0: * when a redirect (301) has occurred.
Chris@0: * TRUE if the file is not found. FALSE if a fault occurred.
Chris@0: */
Chris@0: function locale_translation_http_check($uri) {
Chris@0: $logger = \Drupal::logger('locale');
Chris@0: try {
Chris@0: $actual_uri = NULL;
Chris@0: $response = \Drupal::service('http_client_factory')->fromOptions([
Chris@0: 'allow_redirects' => [
Chris@0: 'on_redirect' => function (RequestInterface $request, ResponseInterface $response, UriInterface $request_uri) use (&$actual_uri) {
Chris@0: $actual_uri = (string) $request_uri;
Chris@17: },
Chris@0: ],
Chris@0: ])->head($uri);
Chris@0: $result = [];
Chris@0:
Chris@0: // Return the effective URL if it differs from the requested.
Chris@0: if ($actual_uri && $actual_uri !== $uri) {
Chris@0: $result['location'] = $actual_uri;
Chris@0: }
Chris@0:
Chris@0: $result['last_modified'] = $response->hasHeader('Last-Modified') ? strtotime($response->getHeaderLine('Last-Modified')) : 0;
Chris@0: return $result;
Chris@0: }
Chris@0: catch (RequestException $e) {
Chris@0: // Handle 4xx and 5xx http responses.
Chris@0: if ($response = $e->getResponse()) {
Chris@0: if ($response->getStatusCode() == 404) {
Chris@0: // File not found occurs when a translation file is not yet available
Chris@0: // at the translation server. But also if a custom module or custom
Chris@0: // theme does not define the location of a translation file. By default
Chris@0: // the file is checked at the translation server, but it will not be
Chris@0: // found there.
Chris@0: $logger->notice('Translation file not found: @uri.', ['@uri' => $uri]);
Chris@0: return TRUE;
Chris@0: }
Chris@0: $logger->notice('HTTP request to @url failed with error: @error.', ['@url' => $uri, '@error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()]);
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: return FALSE;
Chris@0: }
Chris@0:
Chris@0: /**
Chris@0: * Downloads a translation file from a remote server.
Chris@0: *
Chris@0: * @param object $source_file
Chris@0: * Source file object with at least:
Chris@0: * - "uri": uri to download the file from.
Chris@0: * - "project": Project name.
Chris@0: * - "langcode": Translation language.
Chris@0: * - "version": Project version.
Chris@0: * - "filename": File name.
Chris@0: * @param string $directory
Chris@0: * Directory where the downloaded file will be saved. Defaults to the
Chris@0: * temporary file path.
Chris@0: *
Chris@0: * @return object
Chris@0: * File object if download was successful. FALSE on failure.
Chris@0: */
Chris@0: function locale_translation_download_source($source_file, $directory = 'temporary://') {
Chris@16: if ($uri = system_retrieve_file($source_file->uri, $directory, FALSE, FILE_EXISTS_REPLACE)) {
Chris@0: $file = clone($source_file);
Chris@0: $file->type = LOCALE_TRANSLATION_LOCAL;
Chris@0: $file->uri = $uri;
Chris@0: $file->directory = $directory;
Chris@0: $file->timestamp = filemtime($uri);
Chris@0: return $file;
Chris@0: }
Chris@0: \Drupal::logger('locale')->error('Unable to download translation file @uri.', ['@uri' => $source_file->uri]);
Chris@0: return FALSE;
Chris@0: }