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